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,88 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class <%= identity.mailer_class %>Test < ActionMailer::TestCase
|
|
4
|
+
setup { @user = <%= identity.plural %>(:alice) }
|
|
5
|
+
<%- if security_notifications? -%>
|
|
6
|
+
|
|
7
|
+
test "security_notification describes the change without including credentials" do
|
|
8
|
+
mail = <%= identity.mailer_class %>.with(
|
|
9
|
+
to: @user.email,
|
|
10
|
+
event: :password_changed,
|
|
11
|
+
occurred_at: Time.zone.local(2026, 1, 2, 3, 4),
|
|
12
|
+
ip_address: "192.0.2.1",
|
|
13
|
+
user_agent: "Example Browser"
|
|
14
|
+
).security_notification
|
|
15
|
+
|
|
16
|
+
assert_equal [ @user.email ], mail.to
|
|
17
|
+
assert_equal "Your password was changed", mail.subject
|
|
18
|
+
assert_includes mail.body.encoded, "This security change was made to your account."
|
|
19
|
+
assert_includes mail.body.encoded, "192.0.2.1"
|
|
20
|
+
assert_includes mail.body.encoded, "Example Browser"
|
|
21
|
+
end
|
|
22
|
+
<%- end -%>
|
|
23
|
+
<%- if recoverable? -%>
|
|
24
|
+
|
|
25
|
+
test "password_reset links to the reset form" do
|
|
26
|
+
token = @user.generate_token_for(:password_reset)
|
|
27
|
+
mail = <%= identity.mailer_class %>.with(signed_id: token, to: @user.email).password_reset
|
|
28
|
+
|
|
29
|
+
assert_equal [ @user.email ], mail.to
|
|
30
|
+
assert_equal "Reset your password", mail.subject
|
|
31
|
+
expected_url = Rails.application.routes.url_helpers.<%= identity.route_scope %>_reset_password_url(sid: token, host: "example.com")
|
|
32
|
+
assert_includes mail.body.encoded, expected_url
|
|
33
|
+
end
|
|
34
|
+
<%- end -%>
|
|
35
|
+
<%- if email_change_verification? -%>
|
|
36
|
+
|
|
37
|
+
test "email_change_verification describes and links the change" do
|
|
38
|
+
@user.update!(pending_email: "moved@example.com")
|
|
39
|
+
token = @user.generate_token_for(:email_change_verification)
|
|
40
|
+
mail = <%= identity.mailer_class %>.with(signed_id: token, to: @user.pending_email).email_change_verification
|
|
41
|
+
|
|
42
|
+
assert_equal [ "moved@example.com" ], mail.to
|
|
43
|
+
assert_equal "Verify your new email address", mail.subject
|
|
44
|
+
expected_url = Rails.application.routes.url_helpers.<%= identity.route_scope %>_verify_email_change_url(sid: token, host: "example.com")
|
|
45
|
+
assert_includes mail.body.encoded, expected_url
|
|
46
|
+
end
|
|
47
|
+
<%- end -%>
|
|
48
|
+
<%- if email_gated_registration? -%>
|
|
49
|
+
|
|
50
|
+
test "registration_email_verification describes registration" do
|
|
51
|
+
registration = <%= identity.pending_registration_class %>.new(email: "fresh@example.com"<%= staged_registration_invitation(%("fresh@example.com")) %>)
|
|
52
|
+
registration.save!(validate: false)
|
|
53
|
+
token = registration.generate_token_for(:registration_email_verification)
|
|
54
|
+
mail = <%= identity.mailer_class %>.with(signed_id: token, to: registration.email).registration_email_verification
|
|
55
|
+
|
|
56
|
+
assert_equal [ "fresh@example.com" ], mail.to
|
|
57
|
+
assert_equal "Verify your email to finish signing up", mail.subject
|
|
58
|
+
expected_url = Rails.application.routes.url_helpers.<%= identity.route_scope %>_verify_email_url(sid: token, host: "example.com")
|
|
59
|
+
assert_includes mail.body.encoded, expected_url
|
|
60
|
+
end
|
|
61
|
+
<%- end -%>
|
|
62
|
+
<%- if magic_link? -%>
|
|
63
|
+
|
|
64
|
+
test "magic_link links to the sign in url" do
|
|
65
|
+
token = @user.generate_magic_link
|
|
66
|
+
mail = <%= identity.mailer_class %>.with(signed_id: token, to: @user.email).magic_link
|
|
67
|
+
|
|
68
|
+
assert_equal [ @user.email ], mail.to
|
|
69
|
+
assert_equal "Your sign in link", mail.subject
|
|
70
|
+
expected_url = Rails.application.routes.url_helpers.<%= identity.route_scope %>_magic_link_url(sid: token, host: "example.com")
|
|
71
|
+
assert_includes mail.body.encoded, expected_url
|
|
72
|
+
end
|
|
73
|
+
<%- end -%>
|
|
74
|
+
<%- if email_invitable? -%>
|
|
75
|
+
|
|
76
|
+
test "invitation names the inviter and links to the accept page" do
|
|
77
|
+
invitation = <%= identity.invitation_class %>.create!(sent_to: "invitee@example.com", inviter: @user)
|
|
78
|
+
token = invitation.generate_token_for(:invitation)
|
|
79
|
+
mail = <%= identity.mailer_class %>.with(invitation: invitation, signed_id: token, to: invitation.sent_to).invitation
|
|
80
|
+
|
|
81
|
+
assert_equal [ "invitee@example.com" ], mail.to
|
|
82
|
+
assert_equal "You've been invited to #{<%= identity.config_reference %>.app_name}", mail.subject
|
|
83
|
+
assert_includes mail.body.encoded, @user.<%= principals.display.column %>
|
|
84
|
+
expected_url = Rails.application.routes.url_helpers.<%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation" : "accept_invitation" %>_url(token: token, host: "example.com")
|
|
85
|
+
assert_includes mail.body.encoded, expected_url
|
|
86
|
+
end
|
|
87
|
+
<%- end -%>
|
|
88
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class <%= identity.api_token_class %>Test < ActiveSupport::TestCase
|
|
4
|
+
test "only the secret authenticates until expiry or revocation" do
|
|
5
|
+
user = <%= identity.plural %>(:bob)
|
|
6
|
+
token, secret = <%= identity.api_token_class %>.issue!(user:, name: "Script")
|
|
7
|
+
|
|
8
|
+
assert_equal user, <%= identity.api_token_class %>.authenticate(secret).user
|
|
9
|
+
refute_equal secret, token.token_digest
|
|
10
|
+
assert_nil <%= identity.api_token_class %>.authenticate("ae11_#{SecureRandom.urlsafe_base64(32)}")
|
|
11
|
+
assert_nil <%= identity.api_token_class %>.authenticate("not-a-token")
|
|
12
|
+
|
|
13
|
+
token.update!(expires_at: 1.second.ago)
|
|
14
|
+
assert_nil <%= identity.api_token_class %>.authenticate(secret)
|
|
15
|
+
|
|
16
|
+
token.update!(expires_at: 1.day.from_now)
|
|
17
|
+
token.destroy!
|
|
18
|
+
assert_nil <%= identity.api_token_class %>.authenticate(secret)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
test "deleting the account deletes its tokens" do
|
|
22
|
+
user = <%= identity.plural %>(:bob)
|
|
23
|
+
_, secret = <%= identity.api_token_class %>.issue!(user:, name: "Script")
|
|
24
|
+
user.destroy!
|
|
25
|
+
assert_nil <%= identity.api_token_class %>.authenticate(secret)
|
|
26
|
+
end
|
|
27
|
+
<%- if bannable? -%>
|
|
28
|
+
|
|
29
|
+
test "a suspended account cannot use a token" do
|
|
30
|
+
user = <%= identity.plural %>(:bob)
|
|
31
|
+
_, secret = <%= identity.api_token_class %>.issue!(user:, name: "Script")
|
|
32
|
+
user.ban!
|
|
33
|
+
assert_nil <%= identity.api_token_class %>.authenticate(secret)
|
|
34
|
+
user.unban!
|
|
35
|
+
assert_nil <%= identity.api_token_class %>.authenticate(secret)
|
|
36
|
+
end
|
|
37
|
+
<%- end -%>
|
|
38
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class <%= identity.password_history_class %>ReuseTest < ActiveSupport::TestCase
|
|
4
|
+
setup { @user = <%= identity.plural %>(:bob) }
|
|
5
|
+
|
|
6
|
+
test "rejects changing the password to the current one" do
|
|
7
|
+
assert_not @user.update(password: "quilted-lantern-moss-97", password_confirmation: "quilted-lantern-moss-97")
|
|
8
|
+
assert_includes @user.errors[:password], "was used recently. Please choose a different one"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
test "rejects reusing a recently-used password" do
|
|
12
|
+
@user.update!(password: "FirstChange1", password_confirmation: "FirstChange1")
|
|
13
|
+
|
|
14
|
+
assert_not @user.update(password: "quilted-lantern-moss-97", password_confirmation: "quilted-lantern-moss-97")
|
|
15
|
+
assert @user.errors[:password].any?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
test "accepts a genuinely new password and archives the digest it replaced" do
|
|
19
|
+
assert_difference -> { @user.password_histories.count }, 1 do
|
|
20
|
+
assert @user.update(password: "BrandNew1234", password_confirmation: "BrandNew1234"),
|
|
21
|
+
@user.errors.full_messages.to_sentence
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
test "a later save of the same record does not re-check the new password" do
|
|
26
|
+
@user.update!(password: "BrandNew1234", password_confirmation: "BrandNew1234")
|
|
27
|
+
|
|
28
|
+
assert @user.update(updated_at: Time.current), @user.errors.full_messages.to_sentence
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
test "keeps only the configured depth of history" do
|
|
32
|
+
depth = <%= identity.config_reference %>.password.history_depth
|
|
33
|
+
|
|
34
|
+
(1..depth + 2).each do |i|
|
|
35
|
+
@user.update!(password: "Rotation#{i}pw", password_confirmation: "Rotation#{i}pw")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
assert_equal depth, @user.password_histories.count
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
test "forgets a password once it falls past the depth window" do
|
|
42
|
+
depth = <%= identity.config_reference %>.password.history_depth
|
|
43
|
+
|
|
44
|
+
(1..depth + 1).each do |i|
|
|
45
|
+
@user.update!(password: "Rotation#{i}pw", password_confirmation: "Rotation#{i}pw")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
assert @user.update(password: "quilted-lantern-moss-97", password_confirmation: "quilted-lantern-moss-97"),
|
|
49
|
+
@user.errors.full_messages.to_sentence
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class <%= identity.session_class %>Test < ActiveSupport::TestCase
|
|
4
|
+
setup { @user = <%= identity.plural %>(:bob) }
|
|
5
|
+
|
|
6
|
+
test "active and timed_out split sessions by the idle timeout" do
|
|
7
|
+
fresh = @user.sessions.create!
|
|
8
|
+
stale = @user.sessions.create!
|
|
9
|
+
stale.update_column(:updated_at, (<%= identity.config_reference %>.session.idle_timeout + 1.hour).ago)
|
|
10
|
+
|
|
11
|
+
assert_includes <%= identity.session_class %>.active, fresh
|
|
12
|
+
assert_includes <%= identity.session_class %>.timed_out, stale
|
|
13
|
+
refute_includes <%= identity.session_class %>.active, stale
|
|
14
|
+
refute_includes <%= identity.session_class %>.timed_out, fresh
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class <%= identity.class_name %>::SmsChallengeableTest < ActiveSupport::TestCase
|
|
4
|
+
include ActiveJob::TestHelper
|
|
5
|
+
|
|
6
|
+
setup do
|
|
7
|
+
@user = <%= identity.plural %>(:bob)
|
|
8
|
+
Rails.cache.clear
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
test "issuing stores a challenge and texts six digits" do
|
|
12
|
+
assert_enqueued_with(job: SmsDeliveryJob) do
|
|
13
|
+
assert @user.issue_sms_challenge
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
assert @user.sms_challenge?
|
|
17
|
+
assert_match(/\d{6} is your/, enqueued_jobs.last.to_s)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
test "a new delivery replaces the previous code" do
|
|
21
|
+
@user.issue_sms_challenge
|
|
22
|
+
replace_code("xxxxxx")
|
|
23
|
+
|
|
24
|
+
@user.issue_sms_challenge
|
|
25
|
+
|
|
26
|
+
assert_not @user.verify_sms_challenge("xxxxxx")
|
|
27
|
+
assert @user.verify_sms_challenge(issued_code)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
test "a correct code works exactly once" do
|
|
31
|
+
@user.issue_sms_challenge
|
|
32
|
+
code = issued_code
|
|
33
|
+
|
|
34
|
+
assert @user.verify_sms_challenge(code)
|
|
35
|
+
assert_not @user.verify_sms_challenge(code)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
test "a correct code verifies only the number it was texted to" do
|
|
39
|
+
@user.issue_sms_challenge(to: "+14155550100")
|
|
40
|
+
code = issued_code
|
|
41
|
+
|
|
42
|
+
assert_not @user.verify_sms_challenge(code, to: "+14155550199")
|
|
43
|
+
assert @user.verify_sms_challenge(code, to: "+14155550100")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
test "a code waiting for another number is not a challenge for this one" do
|
|
47
|
+
@user.issue_sms_challenge(to: "+14155550100")
|
|
48
|
+
|
|
49
|
+
assert_not @user.sms_challenge?
|
|
50
|
+
assert @user.sms_challenge?(to: "+14155550100")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
test "a code expires" do
|
|
54
|
+
@user.issue_sms_challenge
|
|
55
|
+
code = issued_code
|
|
56
|
+
travel <%= identity.config_reference %>.tokens.sms_code + 1.minute
|
|
57
|
+
|
|
58
|
+
assert_not @user.verify_sms_challenge(code)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
test "wrong codes spend the attempt budget" do
|
|
62
|
+
@user.issue_sms_challenge
|
|
63
|
+
code = issued_code
|
|
64
|
+
|
|
65
|
+
<%= identity.config_reference %>.sms.max_attempts.times do
|
|
66
|
+
assert_not @user.verify_sms_challenge(wrong_code)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
assert_not @user.verify_sms_challenge(code)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
test "a new delivery starts a fresh attempt budget" do
|
|
73
|
+
@user.issue_sms_challenge
|
|
74
|
+
<%= identity.config_reference %>.sms.max_attempts.times do
|
|
75
|
+
@user.verify_sms_challenge(wrong_code)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@user.issue_sms_challenge
|
|
79
|
+
|
|
80
|
+
assert @user.verify_sms_challenge(issued_code)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
test "a missing cache fails closed" do
|
|
84
|
+
cache = Rails.cache
|
|
85
|
+
Rails.cache = ActiveSupport::Cache::NullStore.new
|
|
86
|
+
|
|
87
|
+
@user.issue_sms_challenge
|
|
88
|
+
|
|
89
|
+
assert_not @user.verify_sms_challenge(issued_code)
|
|
90
|
+
ensure
|
|
91
|
+
Rails.cache = cache
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def issued_code
|
|
97
|
+
enqueued_jobs.reverse.filter_map { |job| job.to_s[/(\d{6}) is your/, 1] }.first
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def wrong_code
|
|
101
|
+
issued_code == "000000" ? "000001" : "000000"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def replace_code(code, to: @user.phone)
|
|
105
|
+
Rails.cache.write(@user.send(:sms_challenge_key, :code), { code: code, to: to },
|
|
106
|
+
expires_in: <%= identity.config_reference %>.tokens.sms_code)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class <%= identity.class_name %>::GuestableTest < ActiveSupport::TestCase
|
|
4
|
+
test "create_guest! builds a valid, persisted guest" do
|
|
5
|
+
guest = <%= identity.class_name %>.create_guest!
|
|
6
|
+
|
|
7
|
+
assert guest.persisted?
|
|
8
|
+
assert guest.guest?
|
|
9
|
+
<%- if principals.email -%>
|
|
10
|
+
assert_match(/\Aguest_.+@example\.com\z/, guest.email)
|
|
11
|
+
<%- end -%>
|
|
12
|
+
<%- if principals.username -%>
|
|
13
|
+
assert_match(/\Aguest_[a-zA-Z0-9]+\z/, guest.username)
|
|
14
|
+
<%- end -%>
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
test "create_guest! generates a unique guest each time" do
|
|
18
|
+
assert_not_equal <%= identity.class_name %>.create_guest!.<%= principals.display.column %>, <%= identity.class_name %>.create_guest!.<%= principals.display.column %>
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
test "guests scope returns only guest users" do
|
|
22
|
+
guest = <%= identity.class_name %>.create_guest!
|
|
23
|
+
|
|
24
|
+
assert_includes <%= identity.class_name %>.guests, guest
|
|
25
|
+
assert_not_includes <%= identity.class_name %>.guests, <%= identity.plural %>(:alice)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
test "regular users are not guests" do
|
|
29
|
+
assert_not <%= identity.plural %>(:alice).guest?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
test "expired_guests finds guests past config.guest.ttl, and only those" do
|
|
33
|
+
fresh = <%= identity.class_name %>.create_guest!
|
|
34
|
+
stale = <%= identity.class_name %>.create_guest!
|
|
35
|
+
stale.update_column(:created_at, (<%= identity.config_reference %>.guest.ttl + 1.day).ago)
|
|
36
|
+
|
|
37
|
+
assert_includes <%= identity.class_name %>.expired_guests, stale
|
|
38
|
+
assert_not_includes <%= identity.class_name %>.expired_guests, fresh
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
test "expired_guests never collects a real <%= identity.singular %>, however old" do
|
|
42
|
+
<%= identity.plural %>(:alice).update_column(:created_at, (<%= identity.config_reference %>.guest.ttl + 1.year).ago)
|
|
43
|
+
|
|
44
|
+
assert_not_includes <%= identity.class_name %>.expired_guests, <%= identity.plural %>(:alice)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
test "absorb_guest takes the guest row with it, in one transaction" do
|
|
48
|
+
guest = <%= identity.class_name %>.create_guest!
|
|
49
|
+
|
|
50
|
+
assert_difference -> { <%= identity.class_name %>.guests.count }, -1 do
|
|
51
|
+
<%= identity.plural %>(:alice).absorb_guest(guest)
|
|
52
|
+
end
|
|
53
|
+
assert_nil <%= identity.class_name %>.find_by(id: guest.id)
|
|
54
|
+
end
|
|
55
|
+
<%- if timeoutable? -%>
|
|
56
|
+
|
|
57
|
+
test "<%= identity.session_class %>.timed_out leaves idle guest sessions alone" do
|
|
58
|
+
guest_session = <%= identity.class_name %>.create_guest!.sessions.create!
|
|
59
|
+
<%= identity.singular %>_session = <%= identity.plural %>(:alice).sessions.create!
|
|
60
|
+
idle = (<%= identity.session_class %>.idle_timeout + 1.hour).ago
|
|
61
|
+
[ guest_session, <%= identity.singular %>_session ].each { |s| s.update_column(:updated_at, idle) }
|
|
62
|
+
|
|
63
|
+
assert_includes <%= identity.session_class %>.timed_out, <%= identity.singular %>_session
|
|
64
|
+
assert_not_includes <%= identity.session_class %>.timed_out, guest_session,
|
|
65
|
+
"guests do not time out; config.guest.ttl controls when they are collected"
|
|
66
|
+
end
|
|
67
|
+
<%- end -%>
|
|
68
|
+
<%- if trackable? -%>
|
|
69
|
+
|
|
70
|
+
test "a guest session records no sign-in event" do
|
|
71
|
+
guest = <%= identity.class_name %>.create_guest!
|
|
72
|
+
|
|
73
|
+
assert_no_difference -> { guest.events.count } do
|
|
74
|
+
guest.sessions.create!
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
<%- end -%>
|
|
78
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class <%= identity.class_name %>::RecoveryCodesTest < ActiveSupport::TestCase
|
|
4
|
+
setup do
|
|
5
|
+
@user = <%= identity.plural %>(:bob)
|
|
6
|
+
@user.update!(totp_enrolled_at: Time.current)
|
|
7
|
+
<%= identity.recovery_code_class %>.regenerate_for(@user)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
test "turning the authenticator off keeps the codes while a security key remains" do
|
|
11
|
+
add_security_key
|
|
12
|
+
@user.update!(totp_enrolled_at: nil)
|
|
13
|
+
@user.discard_orphaned_recovery_codes!
|
|
14
|
+
|
|
15
|
+
assert @user.recovery_codes.reload.any?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
test "removing the last security key takes the codes with it" do
|
|
19
|
+
add_security_key
|
|
20
|
+
@user.update!(totp_enrolled_at: nil)
|
|
21
|
+
<%= second_factor_keys_expr("@user") %>.destroy_all
|
|
22
|
+
@user.discard_orphaned_recovery_codes!
|
|
23
|
+
|
|
24
|
+
assert_empty @user.recovery_codes.reload
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def add_security_key
|
|
30
|
+
<%= second_factor_keys_expr("@user") %>.create!(name: "Key", external_id: "ext-#{@user.id}", public_key: "pk"<%= factor_attr(:second_factor) %>)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class <%= identity.class_name %>::SignInMethodsTest < ActiveSupport::TestCase
|
|
4
|
+
setup { @user = <%= identity.plural %>(:bob) }
|
|
5
|
+
<%- if omniauth? -%>
|
|
6
|
+
|
|
7
|
+
test "a linked provider is a way in" do
|
|
8
|
+
@user.omniauth_identities.create!(provider: "developer", uid: "only")
|
|
9
|
+
assert_includes @user.reload.sign_in_methods, :omniauth
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
test "a spare provider is always another way in" do
|
|
13
|
+
@user.omniauth_identities.create!(provider: "developer", uid: "one")
|
|
14
|
+
@user.omniauth_identities.create!(provider: "developer", uid: "two")
|
|
15
|
+
|
|
16
|
+
assert_not @user.reload.last_sign_in_method?(:omniauth),
|
|
17
|
+
"two linked accounts means removing one leaves the other"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
test "last_sign_in_method? is true exactly when this kind is all that is left" do
|
|
21
|
+
@user.omniauth_identities.create!(provider: "developer", uid: "only")
|
|
22
|
+
@user.reload
|
|
23
|
+
|
|
24
|
+
assert_equal (@user.sign_in_methods == [ :omniauth ]), @user.last_sign_in_method?(:omniauth)
|
|
25
|
+
end
|
|
26
|
+
<%- end -%>
|
|
27
|
+
<%- if passkey? && omniauth? -%>
|
|
28
|
+
|
|
29
|
+
test "a provider is protected in a passkey build when no passkey was ever enrolled" do
|
|
30
|
+
@user.webauthn_credentials.destroy_all
|
|
31
|
+
@user.omniauth_identities.destroy_all
|
|
32
|
+
@user.omniauth_identities.create!(provider: "developer", uid: "only")
|
|
33
|
+
@user.reload
|
|
34
|
+
<%- if password? || magic_link? -%>
|
|
35
|
+
|
|
36
|
+
assert_not @user.last_sign_in_method?(:omniauth),
|
|
37
|
+
"this build has another sign-in method, so the link is removable"
|
|
38
|
+
<%- else -%>
|
|
39
|
+
|
|
40
|
+
assert @user.last_sign_in_method?(:omniauth),
|
|
41
|
+
"having passkey sign-in enabled does not mean this account has a passkey"
|
|
42
|
+
<%- end -%>
|
|
43
|
+
end
|
|
44
|
+
<%- end -%>
|
|
45
|
+
<%- if passkey? -%>
|
|
46
|
+
|
|
47
|
+
test "an enrolled passkey is a way in, and the last one is protected" do
|
|
48
|
+
@user.webauthn_credentials<%= ".passkeys" if passkey_and_security_keys? %>.destroy_all
|
|
49
|
+
@user.webauthn_credentials.create!(name: "Only key", external_id: "ext-only", public_key: "pk"<%= factor_attr(:first_factor) %>)
|
|
50
|
+
@user.reload
|
|
51
|
+
|
|
52
|
+
assert_includes @user.sign_in_methods, :passkey
|
|
53
|
+
assert_equal (@user.sign_in_methods == [ :passkey ]), @user.last_sign_in_method?(:passkey)
|
|
54
|
+
end
|
|
55
|
+
<%- end -%>
|
|
56
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class <%= identity.class_name %>Test < ActiveSupport::TestCase
|
|
4
|
+
<%- if principals.email -%>
|
|
5
|
+
test "malformed emails are rejected" do
|
|
6
|
+
user = build_<%= identity.singular %>(email: "person..name@example.com")
|
|
7
|
+
assert_not user.valid?
|
|
8
|
+
assert_includes user.errors[:email], "is invalid"
|
|
9
|
+
end
|
|
10
|
+
<%- end -%>
|
|
11
|
+
<%- if principals.email&.optional? -%>
|
|
12
|
+
|
|
13
|
+
test "email is optional" do
|
|
14
|
+
user = build_<%= identity.singular %>(email: nil)
|
|
15
|
+
assert user.valid?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
test "a blank email normalizes to nil so the partial unique index applies" do
|
|
19
|
+
user = build_<%= identity.singular %>(email: "")
|
|
20
|
+
assert user.save
|
|
21
|
+
assert_nil user.reload.email
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
test "set emails are still unique (enforced by the DB index)" do
|
|
25
|
+
assert build_<%= identity.singular %>(email: "dup@example.com").save
|
|
26
|
+
assert_raises(ActiveRecord::RecordNotUnique) do
|
|
27
|
+
build_<%= identity.singular %>(email: "dup@example.com").save(validate: false)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
<%- end -%>
|
|
31
|
+
<%- if principals.username -%>
|
|
32
|
+
<%- if principals.email&.optional? -%>
|
|
33
|
+
|
|
34
|
+
<%- end -%>
|
|
35
|
+
test "username is required" do
|
|
36
|
+
user = build_<%= identity.singular %>(username: nil)
|
|
37
|
+
assert_not user.valid?
|
|
38
|
+
assert_includes user.errors[:username], "can't be blank"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
test "username uniqueness is honest and case-insensitive" do
|
|
42
|
+
taken = <%= identity.plural %>(:alice).username
|
|
43
|
+
user = build_<%= identity.singular %>(username: taken.upcase)
|
|
44
|
+
assert_not user.valid?
|
|
45
|
+
assert_includes user.errors[:username], "has already been taken"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
test "forbidden usernames are rejected" do
|
|
49
|
+
user = build_<%= identity.singular %>(username: "admin")
|
|
50
|
+
assert_not user.valid?
|
|
51
|
+
assert_includes user.errors[:username], "is forbidden"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
test "the chosen case is preserved on save" do
|
|
55
|
+
user = build_<%= identity.singular %>(username: "MixedCase")
|
|
56
|
+
assert user.save
|
|
57
|
+
assert_equal "MixedCase", user.reload.username
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
test "username uniqueness is also enforced by the DB index" do
|
|
61
|
+
taken = <%= identity.plural %>(:alice).username
|
|
62
|
+
assert_raises(ActiveRecord::RecordNotUnique) do
|
|
63
|
+
build_<%= identity.singular %>(username: taken.upcase).save(validate: false)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
<%- end -%>
|
|
67
|
+
<%- if principals.phone -%>
|
|
68
|
+
<%- if principals.email&.optional? || principals.username -%>
|
|
69
|
+
|
|
70
|
+
<%- end -%>
|
|
71
|
+
test "a valid phone is accepted and normalized to E.164" do
|
|
72
|
+
user = build_<%= identity.singular %>(phone: "+1 415 826 4001")
|
|
73
|
+
assert user.save
|
|
74
|
+
assert_equal "+14158264001", user.reload.phone
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
test "a number typed without a country code is read in the default country" do
|
|
78
|
+
with_default_country("US") do
|
|
79
|
+
user = build_<%= identity.singular %>(phone: "(415) 826-4002")
|
|
80
|
+
assert user.save
|
|
81
|
+
assert_equal "+14158264002", user.reload.phone
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
test "a bare number that isn't valid in the default country is rejected, not reinterpreted" do
|
|
86
|
+
with_default_country("US") do
|
|
87
|
+
user = build_<%= identity.singular %>(phone: "6494461709")
|
|
88
|
+
assert_not user.valid?
|
|
89
|
+
assert_not_equal "+6494461709", user.phone, "must not be read as a New Zealand number"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
test "an explicit country code overrides the default country" do
|
|
94
|
+
with_default_country("US") do
|
|
95
|
+
user = build_<%= identity.singular %>(phone: "+44 20 7183 8750")
|
|
96
|
+
assert user.save
|
|
97
|
+
assert_equal "+442071838750", user.reload.phone
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
test "an array of default countries is tried in order" do
|
|
102
|
+
with_default_country(%w[US GB]) do
|
|
103
|
+
user = build_<%= identity.singular %>(phone: "02071838750")
|
|
104
|
+
assert user.save
|
|
105
|
+
assert_equal "+442071838750", user.reload.phone
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
<%- if principals.phone.required? -%>
|
|
109
|
+
|
|
110
|
+
test "phone is required" do
|
|
111
|
+
assert_not build_<%= identity.singular %>(phone: nil).valid?
|
|
112
|
+
end
|
|
113
|
+
<%- else -%>
|
|
114
|
+
|
|
115
|
+
test "phone is optional" do
|
|
116
|
+
assert build_<%= identity.singular %>(phone: nil).valid?
|
|
117
|
+
end
|
|
118
|
+
<%- end -%>
|
|
119
|
+
|
|
120
|
+
test "set phones are unique (enforced by the DB index)" do
|
|
121
|
+
assert build_<%= identity.singular %>(phone: "+14158264002").save
|
|
122
|
+
assert_raises(ActiveRecord::RecordNotUnique) do
|
|
123
|
+
build_<%= identity.singular %>(phone: "+14158264002").save(validate: false)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
<%- end -%>
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def build_<%= identity.singular %>(**attrs)
|
|
131
|
+
defaults = <%= account_fixture_literal(indent: 4) %>
|
|
132
|
+
<%= identity.class_name %>.new(defaults.merge(attrs))
|
|
133
|
+
end
|
|
134
|
+
<%- if principals.phone -%>
|
|
135
|
+
|
|
136
|
+
def with_default_country(country)
|
|
137
|
+
# Mutate and restore this OrderedOptions value because its method_missing reader cannot be stubbed reliably.
|
|
138
|
+
previous = <%= identity.config_reference %>.phone.default_country
|
|
139
|
+
<%= identity.config_reference %>.phone.default_country = country
|
|
140
|
+
yield
|
|
141
|
+
ensure
|
|
142
|
+
<%= identity.config_reference %>.phone.default_country = previous
|
|
143
|
+
end
|
|
144
|
+
<%- end -%>
|
|
145
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require_relative "authnz_eleven_generator"
|
|
5
|
+
require_relative "../../authnz_eleven/wizard"
|
|
6
|
+
|
|
7
|
+
module AuthnzEleven
|
|
8
|
+
# `bin/rails generate authnz_eleven:wizard`
|
|
9
|
+
#
|
|
10
|
+
# Picks the flags interactively, prints the command it assembled, and offers
|
|
11
|
+
# to run it. The printed command is the point: it goes in a README, and
|
|
12
|
+
# re-running it means the same thing.
|
|
13
|
+
class WizardGenerator < Rails::Generators::Base
|
|
14
|
+
desc "Pick authnz_eleven's flags interactively, then run the generator"
|
|
15
|
+
|
|
16
|
+
# Not a gemspec dependency: huh isn't on RubyGems yet (the `huh` name there
|
|
17
|
+
# belongs to an unrelated library, so don't send anyone to `gem install`),
|
|
18
|
+
# and a gemspec can't declare a git source. It is also wanted exactly once.
|
|
19
|
+
INSTALL = <<~MSG
|
|
20
|
+
The wizard needs the `huh` gem for its terminal forms. Add it to your
|
|
21
|
+
Gemfile, then `bundle install`:
|
|
22
|
+
|
|
23
|
+
gem "huh", github: "marcoroth/huh-ruby", group: :development
|
|
24
|
+
|
|
25
|
+
Or skip the wizard and write the flags yourself:
|
|
26
|
+
|
|
27
|
+
bin/rails generate authnz_eleven --help
|
|
28
|
+
MSG
|
|
29
|
+
|
|
30
|
+
def pick_flags
|
|
31
|
+
@flags = wizard.run
|
|
32
|
+
rescue LoadError
|
|
33
|
+
abort INSTALL
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def report
|
|
37
|
+
say ""
|
|
38
|
+
say " #{wizard.command}", :green
|
|
39
|
+
say ""
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def generate
|
|
43
|
+
return say("Nothing generated.", :yellow) unless yes?("Run it now? [y/N]")
|
|
44
|
+
|
|
45
|
+
AuthnzElevenGenerator.start(@flags)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def wizard = @wizard ||= Wizard.new
|
|
51
|
+
end
|
|
52
|
+
end
|