# Registration Anti-Spam + Email Quota Protection This document describes how the Skinbase email-first registration hardening works. ## Scope Applies to the flow: - `GET /register` - `POST /register` - `GET /register/notice` - `POST /register/resend-verification` - `GET /verify/{token}` - `GET/POST /setup/password` - `GET/POST /setup/username` Primary implementation: - `app/Http/Controllers/Auth/RegisteredUserController.php` - `app/Http/Controllers/Auth/RegistrationVerificationController.php` ## Security Controls ### 1) IP Rate Limiting Defined in `app/Providers/AppServiceProvider.php`: - `register-ip`: per-minute IP limit - `register-ip-daily`: per-day IP limit - `register` (legacy resend route): per-minute IP + per-email key Applied on `POST /register` in `routes/auth.php`: - `throttle:register-ip` - `throttle:register-ip-daily` ### 2) Per-Email Cooldown Cooldown is enforced by user fields: - `users.last_verification_sent_at` - `users.verification_send_count_24h` - `users.verification_send_window_started_at` On repeated requests within cooldown: - No additional verification email is queued - Generic success message is returned ### 3) Progressive CAPTCHA Service: - `app/Services/Security/CaptchaVerifier.php` - `app/Services/Security/TurnstileVerifier.php` (legacy compatibility wrapper) Controller logic (`RegisteredUserController::shouldRequireCaptcha`): - Requires CAPTCHA for suspicious IP activity (attempt threshold) - Also requires CAPTCHA when registration rate-limit state is detected - Active provider is selected through `forum_bot_protection.captcha.provider` UI behavior (`resources/views/auth/register.blade.php`): - Provider-specific widget is only rendered when required - Turnstile, reCAPTCHA, and hCaptcha are supported ### 4) Disposable Domain Block Service: - `app/Services/Auth/DisposableEmailService.php` Config source: - `config/disposable_email_domains.php` Behavior: - Blocks known disposable domains (supports wildcard matching) - Returns friendly validation error ### 5) Queue + Throttle + Quota Circuit Breaker Queue job: - `app/Jobs/SendVerificationEmailJob.php` Behavior: - Registration controller dispatches `SendVerificationEmailJob` - Job applies global send throttling via `RateLimiter` - Job checks monthly quota via `RegistrationEmailQuotaService` - If quota exceeded: send is blocked (fail closed), event marked blocked Quota service/model/table: - `app/Services/Auth/RegistrationEmailQuotaService.php` - `app/Models/SystemEmailQuota.php` - `system_email_quota` Send event audit: - `app/Models/EmailSendEvent.php` - `email_send_events` ### 6) Generic Responses (Anti-Enumeration) The registration entry point uses a standard success message: - `If that email is valid, we sent a verification link.` This message is returned for: - Unknown emails - Existing verified emails - Cooldown cases - Quota-blocked paths ### 7) Verification Token Hardening Service: - `app/Services/Auth/RegistrationVerificationTokenService.php` Protections: - Token generated with high entropy (`Str::random(64)`) - Stored hashed (`sha256`) in `user_verification_tokens` - Expires using configured TTL - Validation uses hash lookup + constant-time compare (`hash_equals`) - Token deleted after successful verification (one-time use) Verification endpoint: - `app/Http/Controllers/Auth/RegistrationVerificationController.php` ## Configuration Main registration config: - `config/registration.php` Key settings: - `ip_per_minute_limit` - `ip_per_day_limit` - `email_per_minute_limit` - `email_cooldown_minutes` - `verify_token_ttl_hours` - `enable_turnstile` - `disposable_domains_enabled` - `turnstile_suspicious_attempts` - `turnstile_attempt_window_minutes` - `email_global_send_per_minute` - `monthly_email_limit` - `generic_success_message` Captcha provider config: - `config/services.php` under `turnstile`, `recaptcha`, and `hcaptcha` - `config/forum_bot_protection.php` under `captcha` Environment examples: - `.env.example` contains all registration anti-spam keys ## Database Objects Added for anti-spam/quota support: - Migration: `2026_02_21_000001_add_registration_antispam_fields_to_users_table.php` - Migration: `2026_02_21_000002_create_email_send_events_table.php` - Migration: `2026_02_21_000003_create_system_email_quota_table.php` - Migration: `2026_02_20_191000_add_registration_phase1_schema.php` (creates `user_verification_tokens`) - Migration: `2026_02_21_000004_rename_token_to_token_hash_in_user_verification_tokens.php` (schema hardening) - Migration: `2026_02_21_000005_ensure_user_verification_tokens_table_exists.php` (rollout safety) ## Test Coverage Primary tests: - `tests/Feature/Auth/RegistrationAntiSpamTest.php` - `tests/Feature/Auth/RegistrationNoticeResendTest.php` - `tests/Feature/Auth/RegistrationQuotaCircuitBreakerTest.php` - `tests/Feature/Auth/RegistrationTokenVerificationTest.php` - `tests/Feature/Auth/RegistrationFlowChecklistTest.php` - `tests/Feature/Auth/RegistrationVerificationMailTest.php` Covered scenarios: - IP rate-limit returns `429` - Cooldown suppresses extra sends - Disposable domains blocked - Quota exceeded blocks send and keeps generic success UX - CAPTCHA required on abuse/rate-limit state - Tokens hashed, expire, and are one-time - Responses avoid account enumeration ## Operations Notes - Keep disposable domain list maintained in `config/disposable_email_domains.php`. - Ensure queue workers process the `mail` queue. - Monitor `email_send_events` for blocked/sent patterns. - Set `REGISTRATION_MONTHLY_EMAIL_LIMIT` based on provider quota. - Configure the active CAPTCHA provider keys in production: - Turnstile: `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY` - reCAPTCHA: `RECAPTCHA_ENABLED`, `RECAPTCHA_SITE_KEY`, `RECAPTCHA_SECRET_KEY` - hCaptcha: `HCAPTCHA_ENABLED`, `HCAPTCHA_SITE_KEY`, `HCAPTCHA_SECRET_KEY`