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
data/lib/generators/authnz_eleven/templates/test_unit/controllers/invitations_controller_test.rb.tt
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class <%= identity.namespaced? ? "#{identity.module_name}::InvitationsController" : "InvitationsController" %>Test < ActionDispatch::IntegrationTest
|
|
4
|
+
include ActiveJob::TestHelper
|
|
5
|
+
|
|
6
|
+
setup { @inviter = <%= identity.plural %>(:alice) }
|
|
7
|
+
<%- if teams? -%>
|
|
8
|
+
|
|
9
|
+
def invite(address, team: <%= identity.team_class %>.create!(name: "Acme"))
|
|
10
|
+
<%= identity.invitation_class %>.create!(sent_to: address, inviter: @inviter, team:)
|
|
11
|
+
end
|
|
12
|
+
<%- else -%>
|
|
13
|
+
|
|
14
|
+
def invite(address) = <%= identity.invitation_class %>.create!(sent_to: address, inviter: @inviter)
|
|
15
|
+
<%- end -%>
|
|
16
|
+
|
|
17
|
+
<%- if teams? -%>
|
|
18
|
+
test "opening the link while signed in spends nothing until it is confirmed" do
|
|
19
|
+
invitee = <%= identity.plural %>(:bob)
|
|
20
|
+
invitation = invite(invitee.<%= email_invitable? ? "email" : "phone" %>)
|
|
21
|
+
sign_in_as invitee
|
|
22
|
+
|
|
23
|
+
get <%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation_path" : "accept_invitation_path" %>(token: invitation.offer)
|
|
24
|
+
|
|
25
|
+
assert_response :success
|
|
26
|
+
assert_not invitation.reload.accepted?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
test "an existing account signs in, comes back to the invitation, and accepts it" do
|
|
30
|
+
invitee = <%= identity.plural %>(:bob)
|
|
31
|
+
invitation = invite(invitee.<%= email_invitable? ? "email" : "phone" %>)
|
|
32
|
+
token = invitation.offer
|
|
33
|
+
|
|
34
|
+
get <%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation_path" : "accept_invitation_path" %>(token: token)
|
|
35
|
+
assert_redirected_to <%= identity.route_scope %>_sign_in_path
|
|
36
|
+
assert_equal <%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation_path" : "accept_invitation_path" %>(token: token), session[:<%= interrupted_destination_key %>]
|
|
37
|
+
|
|
38
|
+
sign_in_as invitee
|
|
39
|
+
assert_not invitation.reload.accepted?, "signing in accepts nothing"
|
|
40
|
+
|
|
41
|
+
patch <%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation_path" : "accept_invitation_path" %>(token: token)
|
|
42
|
+
assert_predicate invitation.reload, :accepted?
|
|
43
|
+
assert invitee.member_of?(invitation.team)
|
|
44
|
+
assert_equal "You've joined Acme.", flash[:notice]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
test "a second invitation to a team already joined is accepted without a second membership" do
|
|
48
|
+
invitee = <%= identity.plural %>(:bob)
|
|
49
|
+
team = <%= identity.team_class %>.create!(name: "Acme")
|
|
50
|
+
first = invite(invitee.<%= email_invitable? ? "email" : "phone" %>, team:)
|
|
51
|
+
second = invite(invitee.<%= email_invitable? ? "email" : "phone" %>, team:)
|
|
52
|
+
|
|
53
|
+
assert first.accept(invitee)
|
|
54
|
+
assert second.accept(invitee)
|
|
55
|
+
assert_equal 1, team.memberships.where(<%= identity.singular %>: invitee).count
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
test "confirming spends the invitation its own link names" do
|
|
59
|
+
invitee = <%= identity.plural %>(:bob)
|
|
60
|
+
mine = invite(invitee.<%= email_invitable? ? "email" : "phone" %>)
|
|
61
|
+
token = mine.offer
|
|
62
|
+
stale = invite(invitee.<%= email_invitable? ? "email" : "phone" %>)
|
|
63
|
+
sign_in_as invitee
|
|
64
|
+
|
|
65
|
+
get <%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation_path" : "accept_invitation_path" %>(token: stale.offer)
|
|
66
|
+
patch <%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation_path" : "accept_invitation_path" %>(token: token)
|
|
67
|
+
|
|
68
|
+
assert mine.reload.accepted?, "the confirmed link is the one that gets spent"
|
|
69
|
+
assert_not stale.reload.accepted?, "the session's last link is not spent by another link's button"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
test "accepting while signed in lands on the invited team" do
|
|
73
|
+
invitee = <%= identity.plural %>(:bob)
|
|
74
|
+
invitee.memberships.create!(team: <%= identity.team_class %>.create!(name: "Original"))
|
|
75
|
+
invitation = invite(invitee.<%= email_invitable? ? "email" : "phone" %>)
|
|
76
|
+
sign_in_as invitee
|
|
77
|
+
|
|
78
|
+
patch <%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation_path" : "accept_invitation_path" %>(token: invitation.offer)
|
|
79
|
+
|
|
80
|
+
<%- if scope_teams? -%>
|
|
81
|
+
assert_redirected_to <%= team_root_helper %>(team_id: invitation.team)
|
|
82
|
+
<%- elsif middleware_teams? -%>
|
|
83
|
+
assert_redirected_to root_url(script_name: "/#{invitation.team.id}")
|
|
84
|
+
<%- else -%>
|
|
85
|
+
assert_equal invitation.team, invitee.sessions.sole.team
|
|
86
|
+
<%- end -%>
|
|
87
|
+
assert_equal "You've joined Acme.", flash[:notice]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
test "an invitation with no team whose address signed up first is accepted and the account still holds for a team" do
|
|
91
|
+
invitee = <%= identity.plural %>(:bob)
|
|
92
|
+
invitee.memberships.destroy_all
|
|
93
|
+
invitation = invite(<%= email_invitable? ? '"stranger-#{SecureRandom.hex(4)}@example.com"' : '"+1415555#{rand(1000..9999)}"' %>, team: nil)
|
|
94
|
+
invitation.update_column(:sent_to, invitee.<%= email_invitable? ? "email" : "phone" %>)
|
|
95
|
+
sign_in_as invitee
|
|
96
|
+
|
|
97
|
+
patch <%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation_path" : "accept_invitation_path" %>(token: invitation.offer)
|
|
98
|
+
|
|
99
|
+
assert invitation.reload.accepted?
|
|
100
|
+
<%- if team_gated_page -%>
|
|
101
|
+
get <%= team_gated_page %>
|
|
102
|
+
assert_redirected_to <%= identity.nested_helper("teams_path") %>
|
|
103
|
+
<%- end -%>
|
|
104
|
+
end
|
|
105
|
+
<%- else -%>
|
|
106
|
+
test "an existing account has nothing to accept" do
|
|
107
|
+
invitee = <%= identity.plural %>(:bob)
|
|
108
|
+
invitation = invite(<%= email_invitable? ? '"stranger-#{SecureRandom.hex(4)}@example.com"' : '"+1415555#{rand(1000..9999)}"' %>)
|
|
109
|
+
sign_in_as invitee
|
|
110
|
+
|
|
111
|
+
get <%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation_path" : "accept_invitation_path" %>(token: invitation.offer)
|
|
112
|
+
|
|
113
|
+
assert_redirected_to <%= identity.namespaced? ? "#{identity.route_scope}_root_path" : "root_path" %>
|
|
114
|
+
assert_not invitation.reload.accepted?
|
|
115
|
+
end
|
|
116
|
+
<%- end -%>
|
|
117
|
+
|
|
118
|
+
test "a tampered invitation link is refused" do
|
|
119
|
+
get <%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation_path" : "accept_invitation_path" %>(token: "not-a-real-token")
|
|
120
|
+
assert_redirected_to <%= identity.namespaced? ? "#{identity.route_scope}_root_path" : "root_path" %>
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
test "a stranger accepting an invitation is routed to create an account" do
|
|
124
|
+
destination = <%= email_invitable? ? '"invitee-#{SecureRandom.hex(4)}@example.com"' : '"+1415555#{rand(1000..9999)}"' %>
|
|
125
|
+
invitation = invite(destination)
|
|
126
|
+
token = invitation.offer
|
|
127
|
+
|
|
128
|
+
get <%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation_path" : "accept_invitation_path" %>(token: token)
|
|
129
|
+
assert_redirected_to <%= registration_entry_path %>
|
|
130
|
+
assert_equal token, session[:<%= session_key(:pending_invitation_token) %>],
|
|
131
|
+
"the session is the only thing carrying the invitation from here on"
|
|
132
|
+
end
|
|
133
|
+
<%- if test_drivable_door? -%>
|
|
134
|
+
|
|
135
|
+
test "signing in drops the invitation from the session" do
|
|
136
|
+
invitation = invite(<%= email_invitable? ? '"invitee-#{SecureRandom.hex(4)}@example.com"' : '"+1415555#{rand(1000..9999)}"' %>)
|
|
137
|
+
get <%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation_path" : "accept_invitation_path" %>(token: invitation.offer)
|
|
138
|
+
|
|
139
|
+
sign_in_as <%= identity.plural %>(:bob)
|
|
140
|
+
|
|
141
|
+
assert_nil session[:<%= session_key(:pending_invitation_token) %>]
|
|
142
|
+
end
|
|
143
|
+
<%- end -%>
|
|
144
|
+
|
|
145
|
+
test "changing the destination invalidates an outstanding link" do
|
|
146
|
+
invitation = invite(<%= email_invitable? ? '"first-#{SecureRandom.hex(4)}@example.com"' : '"+1415555#{rand(1000..9999)}"' %>)
|
|
147
|
+
token = invitation.offer
|
|
148
|
+
invitation.update!(sent_to: <%= email_invitable? ? '"second-#{SecureRandom.hex(4)}@example.com"' : '"+1415555#{rand(1000..9999)}"' %>)
|
|
149
|
+
|
|
150
|
+
assert_nil <%= identity.invitation_class %>.find_by_token_for(:invitation, token)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
test "accepting spends the link" do
|
|
154
|
+
destination = <%= email_invitable? ? '"invitee-#{SecureRandom.hex(4)}@example.com"' : '"+1415555#{rand(1000..9999)}"' %>
|
|
155
|
+
invitation = invite(destination)
|
|
156
|
+
token = invitation.offer
|
|
157
|
+
invitee = <%= identity.class_name %>.create!(<%= account_fixture_literal(indent: 4) %>.merge(<%= email_invitable? ? "email" : "phone" %>: destination))
|
|
158
|
+
invitation.accept(invitee)
|
|
159
|
+
|
|
160
|
+
assert_nil <%= identity.invitation_class %>.find_by_token_for(:invitation, token)
|
|
161
|
+
end
|
|
162
|
+
<%- if typed_registration? -%>
|
|
163
|
+
|
|
164
|
+
test "an invited stranger signs up and the invitation is applied" do
|
|
165
|
+
destination = <%= email_invitable? ? '"invitee-#{SecureRandom.hex(4)}@example.com"' : '"+1415555#{rand(1000..9999)}"' %>
|
|
166
|
+
invitation = invite(destination)
|
|
167
|
+
|
|
168
|
+
get <%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation_path" : "accept_invitation_path" %>(token: invitation.offer)
|
|
169
|
+
assert_redirected_to <%= identity.route_scope %>_sign_up_path
|
|
170
|
+
|
|
171
|
+
post <%= identity.route_scope %>_sign_up_path, params: sign_up_params(<%= email_invitable? ? "email" : "phone" %>: destination)
|
|
172
|
+
|
|
173
|
+
<%- unless email_invitable? ? email_invitation_completes_registration? : phone_invitation_completes_registration? -%>
|
|
174
|
+
assert_nil <%= identity.class_name %>.find_by(<%= email_invitable? ? "email" : "phone" %>: destination)
|
|
175
|
+
assert <%= identity.pending_registration_class %>.exists?
|
|
176
|
+
assert_not_predicate invitation.reload, :accepted?
|
|
177
|
+
<%- else -%>
|
|
178
|
+
<%= identity.singular %> = <%= identity.class_name %>.find_by(<%= email_invitable? ? "email" : "phone" %>: destination)
|
|
179
|
+
assert <%= identity.singular %>, "clicking the invitation verifies the address, so the account is minted here"
|
|
180
|
+
assert_predicate invitation.reload, :accepted?
|
|
181
|
+
assert_equal <%= identity.singular %>, invitation.invitee
|
|
182
|
+
<%- if teams? -%>
|
|
183
|
+
assert <%= identity.singular %>.member_of?(invitation.team)
|
|
184
|
+
<%- end -%>
|
|
185
|
+
<%- end -%>
|
|
186
|
+
end
|
|
187
|
+
<%- if phone_invitable? -%>
|
|
188
|
+
|
|
189
|
+
test "a phone invitation signs up without sending a verification code" do
|
|
190
|
+
number = "+1415555#{rand(1000..9999)}"
|
|
191
|
+
invitation = invite(number)
|
|
192
|
+
token = invitation.offer
|
|
193
|
+
clear_enqueued_jobs
|
|
194
|
+
|
|
195
|
+
get <%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation_path" : "accept_invitation_path" %>(token: token)
|
|
196
|
+
assert_redirected_to <%= identity.route_scope %>_sign_up_path
|
|
197
|
+
|
|
198
|
+
assert_no_enqueued_jobs only: SmsDeliveryJob do
|
|
199
|
+
post <%= identity.route_scope %>_sign_up_path, params: sign_up_params.except(:phone)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
<%- unless phone_invitation_completes_registration? -%>
|
|
203
|
+
assert_nil <%= identity.class_name %>.find_by(phone: number)
|
|
204
|
+
assert <%= identity.pending_registration_class %>.exists?
|
|
205
|
+
assert_not_predicate invitation.reload, :accepted?
|
|
206
|
+
<%- else -%>
|
|
207
|
+
<%= identity.singular %> = <%= identity.class_name %>.find_by(phone: number)
|
|
208
|
+
assert <%= identity.singular %>
|
|
209
|
+
assert_predicate invitation.reload, :accepted?
|
|
210
|
+
assert_equal <%= identity.singular %>, invitation.invitee
|
|
211
|
+
<%- if teams? -%>
|
|
212
|
+
assert <%= identity.singular %>.member_of?(invitation.team)
|
|
213
|
+
<%- end -%>
|
|
214
|
+
<%- end -%>
|
|
215
|
+
end
|
|
216
|
+
<%- end -%>
|
|
217
|
+
<%- end -%>
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
<%- if typed_registration? -%>
|
|
221
|
+
|
|
222
|
+
def sign_up_params(**overrides)
|
|
223
|
+
<%= sign_up_fixture_literal(indent: 4) %>.merge(overrides)
|
|
224
|
+
end
|
|
225
|
+
<%- end -%>
|
|
226
|
+
|
|
227
|
+
def sign_in_as(user)
|
|
228
|
+
<%= test_sign_in_body %>
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
require "webauthn/fake_client"
|
|
3
|
+
|
|
4
|
+
class <%= identity.module_name %>::PasskeyCeremonyTest < ActionDispatch::IntegrationTest
|
|
5
|
+
setup do
|
|
6
|
+
@user = <%= identity.plural %>(:bob)
|
|
7
|
+
@authenticator = WebAuthn::FakeAuthenticator.new
|
|
8
|
+
@client = WebAuthn::FakeClient.new(
|
|
9
|
+
WebAuthn.configuration.allowed_origins.first,
|
|
10
|
+
authenticator: @authenticator
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
test "passkey enrollment requires user verification" do
|
|
15
|
+
sign_in @user
|
|
16
|
+
|
|
17
|
+
assert_no_difference -> { @user.webauthn_credentials.count } do
|
|
18
|
+
enroll_passkey(user_verified: false)
|
|
19
|
+
end
|
|
20
|
+
assert_response :unprocessable_entity
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
test "a verified passkey enrolls and signs in" do
|
|
24
|
+
sign_in @user
|
|
25
|
+
assert_difference -> { @user.webauthn_credentials.count }, 1 do
|
|
26
|
+
enroll_passkey(user_verified: true)
|
|
27
|
+
end
|
|
28
|
+
assert_response :created
|
|
29
|
+
|
|
30
|
+
sign_out
|
|
31
|
+
assert_difference -> { @user.sessions.count }, 1 do
|
|
32
|
+
sign_in_with_passkey(user_verified: true)
|
|
33
|
+
end
|
|
34
|
+
assert_response :created
|
|
35
|
+
assert cookies[:<%= identity.cookie_name %>].present?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
test "a replayed passkey assertion is refused, not an error" do
|
|
39
|
+
sign_in @user
|
|
40
|
+
enroll_passkey(user_verified: true)
|
|
41
|
+
sign_out
|
|
42
|
+
|
|
43
|
+
credential = sign_in_with_passkey(user_verified: true)
|
|
44
|
+
assert_response :created
|
|
45
|
+
sign_out
|
|
46
|
+
|
|
47
|
+
assert_no_difference -> { @user.sessions.count } do
|
|
48
|
+
post <%= identity.route_scope %>_passkeys_session_path, params: { credential: credential }, as: :json
|
|
49
|
+
end
|
|
50
|
+
assert_response :unprocessable_entity
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
test "passkey sign-in requires user verification" do
|
|
54
|
+
sign_in @user
|
|
55
|
+
enroll_passkey(user_verified: true)
|
|
56
|
+
sign_out
|
|
57
|
+
|
|
58
|
+
assert_no_difference -> { @user.sessions.count } do
|
|
59
|
+
sign_in_with_passkey(user_verified: false)
|
|
60
|
+
end
|
|
61
|
+
assert_response :unprocessable_entity
|
|
62
|
+
assert cookies[:<%= identity.cookie_name %>].blank?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
<%- if sudoable? -%>
|
|
66
|
+
|
|
67
|
+
test "a passkey satisfies the sudo challenge on a guarded action" do
|
|
68
|
+
sign_in @user
|
|
69
|
+
enroll_passkey(user_verified: true)
|
|
70
|
+
|
|
71
|
+
assert_no_difference -> { <%= identity.class_name %>.count }, "no assertion, no deletion" do
|
|
72
|
+
delete <%= identity.settings_helper_prefix %>_user_path, params: { confirmation: "DELETE ACCOUNT" }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
assert_difference -> { <%= identity.class_name %>.count }, -1 do
|
|
76
|
+
delete <%= identity.settings_helper_prefix %>_user_path,
|
|
77
|
+
params: { confirmation: "DELETE ACCOUNT", sudo_credential: <%= identity.route_scope %>_sudo_assertion }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
<%- end -%>
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def clear_sudo_bar
|
|
85
|
+
<%= test_clear_sudo_body %>
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def enroll_passkey(user_verified:)
|
|
89
|
+
clear_sudo_bar
|
|
90
|
+
get new_<%= identity.settings_helper_prefix %>_passkey_path, as: :json
|
|
91
|
+
credential = @client.create(
|
|
92
|
+
challenge: response.parsed_body.fetch("challenge"),
|
|
93
|
+
rp_id: WebAuthn.configuration.rp_id,
|
|
94
|
+
user_verified: user_verified
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
post <%= identity.settings_helper_prefix %>_passkeys_path,
|
|
98
|
+
params: { credential: credential }, as: :json
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def sign_in_with_passkey(user_verified:)
|
|
102
|
+
get new_<%= identity.route_scope %>_passkeys_session_path, as: :json
|
|
103
|
+
credential = @client.get(
|
|
104
|
+
challenge: response.parsed_body.fetch("challenge"),
|
|
105
|
+
rp_id: WebAuthn.configuration.rp_id,
|
|
106
|
+
user_handle: @user.webauthn_handle,
|
|
107
|
+
user_verified: user_verified
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
post <%= identity.route_scope %>_passkeys_session_path,
|
|
111
|
+
params: { credential: credential }, as: :json
|
|
112
|
+
credential
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def sign_in(user)
|
|
116
|
+
record = user.sessions.create!
|
|
117
|
+
env = Rails.application.env_config.merge("HTTP_HOST" => "www.example.com")
|
|
118
|
+
jar = ActionDispatch::Request.new(env).cookie_jar
|
|
119
|
+
jar.signed[:<%= identity.cookie_name %>] = record.id
|
|
120
|
+
cookies[:<%= identity.cookie_name %>] = jar[:<%= identity.cookie_name %>]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def sign_out
|
|
124
|
+
delete <%= identity.route_scope %>_sign_out_path
|
|
125
|
+
end
|
|
126
|
+
end
|
data/lib/generators/authnz_eleven/templates/test_unit/controllers/password_rotation_test.rb.tt
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class <%= identity.module_name %>::PasswordRotationTest < ActionDispatch::IntegrationTest
|
|
4
|
+
setup { @user = <%= identity.plural %>(:bob) }
|
|
5
|
+
|
|
6
|
+
test "a <%= identity.singular %> with an expired password is held at the change-password page" do
|
|
7
|
+
expire_password
|
|
8
|
+
assert_not @user.reload.password_fresh?
|
|
9
|
+
sign_in_as @user
|
|
10
|
+
|
|
11
|
+
get <%= identity.settings_helper_prefix %>_path
|
|
12
|
+
assert_redirected_to edit_<%= identity.settings_helper_prefix %>_password_path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
test "the change-password page itself stays reachable while held" do
|
|
16
|
+
expire_password
|
|
17
|
+
sign_in_as @user
|
|
18
|
+
|
|
19
|
+
get edit_<%= identity.settings_helper_prefix %>_password_path
|
|
20
|
+
assert_response :success
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
test "signing out is allowed even while held (never trapped in purgatory)" do
|
|
24
|
+
expire_password
|
|
25
|
+
sign_in_as @user
|
|
26
|
+
|
|
27
|
+
assert_difference -> { @user.sessions.count }, -1 do
|
|
28
|
+
delete <%= identity.route_scope %>_sign_out_path
|
|
29
|
+
end
|
|
30
|
+
assert_response :redirect
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
test "a <%= identity.singular %> with a current password passes the gate" do
|
|
34
|
+
assert @user.password_fresh?, "fixture password should start fresh"
|
|
35
|
+
sign_in_as @user
|
|
36
|
+
|
|
37
|
+
get <%= identity.settings_helper_prefix %>_path
|
|
38
|
+
assert_response :success
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
test "changing the password clears the gate" do
|
|
42
|
+
expire_password
|
|
43
|
+
sign_in_as @user
|
|
44
|
+
|
|
45
|
+
patch <%= identity.settings_helper_prefix %>_password_path, params: {
|
|
46
|
+
password_challenge: "quilted-lantern-moss-97",
|
|
47
|
+
password: "amber-thicket-90-vale",
|
|
48
|
+
password_confirmation: "amber-thicket-90-vale"
|
|
49
|
+
}
|
|
50
|
+
assert @user.reload.password_fresh?, "a fresh password stamps password_changed_at"
|
|
51
|
+
|
|
52
|
+
get <%= identity.settings_helper_prefix %>_path
|
|
53
|
+
assert_response :success
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def expire_password
|
|
59
|
+
# Bypass the password-change callback so it does not replace the backdated timestamp.
|
|
60
|
+
@user.update_column(:password_changed_at, (<%= identity.config_reference %>.password.maximum_age + 1.day).ago)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def sign_in_as(user)
|
|
64
|
+
post <%= identity.route_scope %>_sign_in_path,
|
|
65
|
+
params: { <%= sign_in_field %>: user.<%= login_field %>, password: "quilted-lantern-moss-97" }
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/generators/authnz_eleven/templates/test_unit/controllers/security_key_ceremony_test.rb.tt
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
require "webauthn/fake_client"
|
|
3
|
+
|
|
4
|
+
class <%= identity.module_name %>::SecurityKeyCeremonyTest < ActionDispatch::IntegrationTest
|
|
5
|
+
setup do
|
|
6
|
+
@user = <%= identity.plural %>(:bob)
|
|
7
|
+
@authenticator = WebAuthn::FakeAuthenticator.new
|
|
8
|
+
@client = WebAuthn::FakeClient.new(
|
|
9
|
+
WebAuthn.configuration.allowed_origins.first,
|
|
10
|
+
authenticator: @authenticator
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
test "a security key enrolls as a possession-only second factor" do
|
|
15
|
+
sign_in @user
|
|
16
|
+
|
|
17
|
+
assert_difference -> { @user.webauthn_credentials.count }, 1 do
|
|
18
|
+
enroll_security_key
|
|
19
|
+
end
|
|
20
|
+
assert_response :success
|
|
21
|
+
assert_equal "text/html", response.media_type
|
|
22
|
+
|
|
23
|
+
codes = response.body.scan(%r{<code>([a-z0-9]+)</code>}).flatten
|
|
24
|
+
assert_equal <%= identity.config_reference %>.second_factor.recovery_code_count, codes.length
|
|
25
|
+
assert_equal codes.length, @user.recovery_codes.count
|
|
26
|
+
assert <%= identity.recovery_code_class %>.redeem(user: @user, code: codes.first)
|
|
27
|
+
|
|
28
|
+
get <%= identity.settings_helper_prefix %>_mfa_security_keys_path
|
|
29
|
+
assert_response :success
|
|
30
|
+
codes.each { |code| assert_not_includes response.body, code }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
test "the enrollment form opts out of Turbo" do
|
|
34
|
+
sign_in @user
|
|
35
|
+
clear_sudo_bar
|
|
36
|
+
get new_<%= identity.settings_helper_prefix %>_mfa_security_key_path
|
|
37
|
+
|
|
38
|
+
assert_select 'form[action=?][method="post"][data-turbo="false"]', <%= identity.settings_helper_prefix %>_mfa_security_keys_path
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
test "another key preserves the existing recovery codes" do
|
|
42
|
+
sign_in @user
|
|
43
|
+
enroll_security_key
|
|
44
|
+
codes = @user.recovery_codes.pluck(:code_digest).sort
|
|
45
|
+
assert_not_empty codes
|
|
46
|
+
|
|
47
|
+
assert_difference -> { @user.webauthn_credentials.count }, 1 do
|
|
48
|
+
enroll_security_key
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
assert_response :see_other
|
|
52
|
+
assert_redirected_to edit_<%= identity.settings_helper_prefix %>_mfa_security_key_path(@user.webauthn_credentials.order(:id).last)
|
|
53
|
+
assert_equal codes, @user.recovery_codes.reload.pluck(:code_digest).sort
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
test "a recovery-code failure rolls back enrollment" do
|
|
57
|
+
sign_in @user
|
|
58
|
+
credential = security_key_credential
|
|
59
|
+
|
|
60
|
+
assert_no_difference -> { @user.webauthn_credentials.count } do
|
|
61
|
+
failing_recovery_codes do
|
|
62
|
+
assert_raises(RuntimeError) do
|
|
63
|
+
post <%= identity.settings_helper_prefix %>_mfa_security_keys_path, params: { credential: credential.to_json }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
assert_empty @user.recovery_codes.reload
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
test "malformed enrollment renders a retry form" do
|
|
71
|
+
sign_in @user
|
|
72
|
+
security_key_credential
|
|
73
|
+
|
|
74
|
+
[ nil, "not json" ].each do |malformed|
|
|
75
|
+
assert_no_difference -> { @user.webauthn_credentials.count } do
|
|
76
|
+
post <%= identity.settings_helper_prefix %>_mfa_security_keys_path, params: { credential: malformed }
|
|
77
|
+
end
|
|
78
|
+
assert_response :unprocessable_entity
|
|
79
|
+
assert_select '[data-webauthn-target="field"]'
|
|
80
|
+
assert_select "p", text: "We couldn't add that security key. Please try again."
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
enroll_security_key
|
|
84
|
+
assert_response :success
|
|
85
|
+
end
|
|
86
|
+
<%- if admin_mfa_gate? && password_rotatable? -%>
|
|
87
|
+
|
|
88
|
+
test "MFA enrollment displays recovery codes before the next requirement takes over" do
|
|
89
|
+
@user = <%= identity.plural %>(:alice)
|
|
90
|
+
@user.update_column(:password_changed_at, (<%= identity.config_reference %>.password.maximum_age + 1.day).ago)
|
|
91
|
+
sign_in @user
|
|
92
|
+
|
|
93
|
+
get <%= identity.settings_helper_prefix %>_path
|
|
94
|
+
assert_redirected_to <%= admin_mfa_hold_path %>
|
|
95
|
+
|
|
96
|
+
enroll_security_key
|
|
97
|
+
assert_response :success
|
|
98
|
+
assert @user.reload.second_factor_enrolled?
|
|
99
|
+
assert_not @user.password_fresh?
|
|
100
|
+
assert_select "h1", "Save your recovery codes"
|
|
101
|
+
assert_select "li code", count: <%= identity.config_reference %>.second_factor.recovery_code_count
|
|
102
|
+
assert_nil flash[:alert]
|
|
103
|
+
|
|
104
|
+
post complete_<%= identity.settings_helper_prefix %>_mfa_recovery_codes_path
|
|
105
|
+
assert_redirected_to edit_<%= identity.settings_helper_prefix %>_password_path
|
|
106
|
+
end
|
|
107
|
+
<%- end -%>
|
|
108
|
+
|
|
109
|
+
test "a replayed enrollment is refused, not an error" do
|
|
110
|
+
sign_in @user
|
|
111
|
+
credential = enroll_security_key
|
|
112
|
+
assert_response :success
|
|
113
|
+
|
|
114
|
+
assert_no_difference -> { @user.webauthn_credentials.count } do
|
|
115
|
+
post <%= identity.settings_helper_prefix %>_mfa_security_keys_path,
|
|
116
|
+
params: { credential: credential.to_json }
|
|
117
|
+
end
|
|
118
|
+
assert_response :unprocessable_entity
|
|
119
|
+
end
|
|
120
|
+
<%- if password? -%>
|
|
121
|
+
|
|
122
|
+
test "a security key clears the second-factor challenge" do
|
|
123
|
+
sign_in @user
|
|
124
|
+
enroll_security_key
|
|
125
|
+
sign_out
|
|
126
|
+
|
|
127
|
+
start_first_factor(@user)
|
|
128
|
+
|
|
129
|
+
get new_<%= identity.route_scope %>_mfa_challenge_security_keys_path, as: :json
|
|
130
|
+
credential = @client.get(
|
|
131
|
+
challenge: response.parsed_body.fetch("challenge"),
|
|
132
|
+
rp_id: WebAuthn.configuration.rp_id,
|
|
133
|
+
user_verified: false
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
assert_difference -> { @user.sessions.count }, 1 do
|
|
137
|
+
post <%= identity.route_scope %>_mfa_challenge_security_keys_path,
|
|
138
|
+
params: { credential: credential }, as: :json
|
|
139
|
+
end
|
|
140
|
+
assert_response :created
|
|
141
|
+
assert cookies[:<%= identity.cookie_name %>].present?
|
|
142
|
+
end
|
|
143
|
+
<%- end -%>
|
|
144
|
+
|
|
145
|
+
test "a security key can be removed" do
|
|
146
|
+
sign_in @user
|
|
147
|
+
enroll_security_key
|
|
148
|
+
|
|
149
|
+
assert_difference -> { @user.webauthn_credentials.count }, -1 do
|
|
150
|
+
delete <%= identity.settings_helper_prefix %>_mfa_security_key_path(@user.webauthn_credentials.sole)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
<%- if sudoable? -%>
|
|
154
|
+
|
|
155
|
+
test "a security key satisfies the sudo challenge on a guarded action" do
|
|
156
|
+
sign_in @user
|
|
157
|
+
enroll_security_key
|
|
158
|
+
|
|
159
|
+
assert_no_difference -> { <%= identity.class_name %>.count }, "no assertion, no deletion" do
|
|
160
|
+
delete <%= identity.settings_helper_prefix %>_user_path, params: { confirmation: "DELETE ACCOUNT" }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
assert_difference -> { <%= identity.class_name %>.count }, -1 do
|
|
164
|
+
delete <%= identity.settings_helper_prefix %>_user_path,
|
|
165
|
+
params: { confirmation: "DELETE ACCOUNT", sudo_credential: <%= identity.route_scope %>_sudo_assertion }
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
<%- end -%>
|
|
169
|
+
|
|
170
|
+
private
|
|
171
|
+
|
|
172
|
+
def failing_recovery_codes
|
|
173
|
+
original = <%= identity.recovery_code_class %>.method(:regenerate_for)
|
|
174
|
+
<%= identity.recovery_code_class %>.define_singleton_method(:regenerate_for) { |*| raise "Recovery codes unavailable" }
|
|
175
|
+
yield
|
|
176
|
+
ensure
|
|
177
|
+
<%= identity.recovery_code_class %>.define_singleton_method(:regenerate_for, original)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def clear_sudo_bar
|
|
181
|
+
<%= test_clear_sudo_body %>
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def enroll_security_key
|
|
185
|
+
credential = security_key_credential
|
|
186
|
+
post <%= identity.settings_helper_prefix %>_mfa_security_keys_path,
|
|
187
|
+
params: { credential: credential.to_json }
|
|
188
|
+
credential
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def security_key_credential
|
|
192
|
+
clear_sudo_bar
|
|
193
|
+
get new_<%= identity.settings_helper_prefix %>_mfa_security_key_path, as: :json
|
|
194
|
+
@client.create(
|
|
195
|
+
challenge: response.parsed_body.fetch("challenge"),
|
|
196
|
+
rp_id: WebAuthn.configuration.rp_id,
|
|
197
|
+
user_verified: false # User verification is optional here because the password is the first factor.
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def sign_in(user)
|
|
202
|
+
record = user.sessions.create!
|
|
203
|
+
env = Rails.application.env_config.merge("HTTP_HOST" => "www.example.com")
|
|
204
|
+
jar = ActionDispatch::Request.new(env).cookie_jar
|
|
205
|
+
jar.signed[:<%= identity.cookie_name %>] = record.id
|
|
206
|
+
cookies[:<%= identity.cookie_name %>] = jar[:<%= identity.cookie_name %>]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def sign_out
|
|
210
|
+
delete <%= identity.route_scope %>_sign_out_path
|
|
211
|
+
end
|
|
212
|
+
<%- if password? -%>
|
|
213
|
+
|
|
214
|
+
def start_first_factor(user)
|
|
215
|
+
<%= test_sign_in_body %>
|
|
216
|
+
end
|
|
217
|
+
<%- end -%>
|
|
218
|
+
end
|
data/lib/generators/authnz_eleven/templates/test_unit/controllers/security_notifications_test.rb.tt
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class <%= identity.module_name %>::SecurityNotificationsTest < ActionDispatch::IntegrationTest
|
|
4
|
+
include ActionMailer::TestHelper
|
|
5
|
+
|
|
6
|
+
setup { @user = <%= identity.plural %>(:bob) }
|
|
7
|
+
|
|
8
|
+
test "revoking a session emails the account" do
|
|
9
|
+
sign_in_as @user
|
|
10
|
+
other_session = @user.sessions.create!
|
|
11
|
+
|
|
12
|
+
assert_security_notification(:session_revoked) do
|
|
13
|
+
delete <%= identity.settings_helper_prefix %>_session_path(other_session)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
<%- if password? -%>
|
|
17
|
+
|
|
18
|
+
test "changing the password emails the account" do
|
|
19
|
+
sign_in_as @user
|
|
20
|
+
|
|
21
|
+
assert_security_notification(:password_changed) do
|
|
22
|
+
patch <%= identity.settings_helper_prefix %>_password_path,
|
|
23
|
+
params: { password_challenge: "quilted-lantern-moss-97", password: "heron-basalt-drift-41",
|
|
24
|
+
password_confirmation: "heron-basalt-drift-41" }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
<%- end -%>
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def assert_security_notification(event, &block)
|
|
32
|
+
expected = ->(params) { params[:to] == @user.email && params[:event] == event }
|
|
33
|
+
assert_enqueued_email_with <%= identity.mailer_class %>, :security_notification, params: expected, &block
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def sign_in_as(user)
|
|
37
|
+
<%= test_sign_in_body %>
|
|
38
|
+
end
|
|
39
|
+
end
|