5.3 KiB
Registration Anti-Spam + Email Quota Protection
This document describes how the Skinbase email-first registration hardening works.
Scope
Applies to the flow:
GET /registerPOST /registerGET /register/noticePOST /register/resend-verificationGET /verify/{token}GET/POST /setup/passwordGET/POST /setup/username
Primary implementation:
app/Http/Controllers/Auth/RegisteredUserController.phpapp/Http/Controllers/Auth/RegistrationVerificationController.php
Security Controls
1) IP Rate Limiting
Defined in app/Providers/AppServiceProvider.php:
register-ip: per-minute IP limitregister-ip-daily: per-day IP limitregister(legacy resend route): per-minute IP + per-email key
Applied on POST /register in routes/auth.php:
throttle:register-ipthrottle:register-ip-daily
2) Per-Email Cooldown
Cooldown is enforced by user fields:
users.last_verification_sent_atusers.verification_send_count_24husers.verification_send_window_started_at
On repeated requests within cooldown:
- No additional verification email is queued
- Generic success message is returned
3) Progressive CAPTCHA (Turnstile)
Service:
app/Services/Security/TurnstileVerifier.php
Controller logic (RegisteredUserController::shouldRequireTurnstile):
- Requires Turnstile for suspicious IP activity (attempt threshold)
- Also requires Turnstile when registration rate-limit state is detected
UI behavior (resources/views/auth/register.blade.php):
- Turnstile widget is only rendered when required
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.phpapp/Models/SystemEmailQuota.phpsystem_email_quota
Send event audit:
app/Models/EmailSendEvent.phpemail_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) inuser_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_limitip_per_day_limitemail_per_minute_limitemail_cooldown_minutesverify_token_ttl_hoursenable_turnstiledisposable_domains_enabledturnstile_suspicious_attemptsturnstile_attempt_window_minutesemail_global_send_per_minutemonthly_email_limitgeneric_success_message
Turnstile config:
config/services.phpunderturnstile
Environment examples:
.env.examplecontains 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(createsuser_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.phptests/Feature/Auth/RegistrationNoticeResendTest.phptests/Feature/Auth/RegistrationQuotaCircuitBreakerTest.phptests/Feature/Auth/RegistrationTokenVerificationTest.phptests/Feature/Auth/RegistrationFlowChecklistTest.phptests/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
- Turnstile 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
mailqueue. - Monitor
email_send_eventsfor blocked/sent patterns. - Set
REGISTRATION_MONTHLY_EMAIL_LIMITbased on provider quota. - Configure
TURNSTILE_SITE_KEYandTURNSTILE_SECRET_KEYin production.