# Supuestos F8 — Checklist de seguridad

## 1. Cabeceras de seguridad HTTP

- Nueva middleware `App\Http\Middleware\SecurityHeaders`, registrada en dos
  sitios (necesario porque Filament tiene su propio stack de middleware,
  separado del grupo `web` global de Laravel):
  - `bootstrap/app.php` → `$middleware->web(append: [SecurityHeaders::class])`.
  - `AdminPanelProvider::panel()->middleware([...])`.
- Elimina `X-Powered-By`. Requiere tanto `$response->headers->remove(...)`
  (bolsa de cabeceras de Symfony) como la función nativa `header_remove(...)`
  de PHP, porque `expose_php` en php.ini añade esa cabecera a nivel de SAPI,
  fuera del control de Symfony. Verificado con `curl -I` real contra Apache
  local, no solo con el test.
- Añade `Referrer-Policy: strict-origin-when-cross-origin` y
  `X-Content-Type-Options: nosniff`.
- Test: `tests/Feature/SecurityHeadersTest.php`.

## 2. Autenticación de doble factor (2FA)

- `pragmarx/google2fa-qrcode` (ya presente como dependencia transitiva de
  `pragmarx/google2fa-laravel`) para generar el secreto y el QR inline
  (data URL, sin llamada de red externa). NO se usa el wrapper
  `pragmarx/google2fa-laravel` directamente — su `Middleware`/`Authenticator`
  asumen un ciclo petición-respuesta clásico incompatible con el panel de
  Filament (Livewire). NO se usa el 2FA nativo de Fortify (ya deshabilitado
  explícitamente en `config/fortify.php`, ver CLAUDE.md).
- `App\Services\TwoFactorAuthService`: envoltorio fino sobre
  `PragmaRX\Google2FAQRCode\Google2FA` (`generateSecret()`,
  `qrCodeDataUrl($email, $secret)`, `verify($secret, $code)`).
- `App\Filament\Pages\TwoFactorSettings`: página de ajustes de usuario
  (activar/desactivar). Activar exige verificar un código válido antes de
  persistir `google2fa_enabled=true` (evita bloquear al usuario con un
  secreto que no coincide con lo escaneado). Desactivar limpia también
  `google2fa_secret` (no solo el flag), para mantener ambos campos
  consistentes con `Google2FA::isActivated()`.
- `App\Filament\Pages\Auth\TwoFactorChallenge`: pantalla de desafío en el
  login, marca `session('two_factor_verified_user_id')` al verificar.
- `App\Http\Middleware\RequireTwoFactorChallenge`: registrada en
  `AdminPanelProvider->authMiddleware()`, después de `EnforceAutoLogout` y
  antes de `EnforcePasswordChange`. Si `google2fa_enabled=true` y la sesión
  no tiene el `user->id` verificado, redirige SIEMPRE a `TwoFactorChallenge`
  — un usuario no puede saltárselo navegando directamente a otra URL del
  panel (verificado con test dedicado).
- Enlace en el menú de usuario de Filament (`userMenuItems()`), traducido
  íntegramente en `lang/de/admin.php` y `lang/es/admin.php` bajo la clave
  `two_factor` (regla CLAUDE.md — ningún string nuevo sin traducir en el
  mismo commit).
- **Bug real encontrado y corregido**: `User::$fillable` no incluía
  `google2fa_secret` ni `google2fa_enabled`, así que `$user->update([...])`
  descartaba esos campos en silencio (Laravel no lanza excepción por
  mass-assignment guardado salvo que `preventSilentlyDiscardingAttributes()`
  esté activo, que no lo está en este proyecto). El código de
  `TwoFactorSettings` llegaba a su rama de éxito (limpiaba `pendingSecret`,
  mostraba notificación de éxito) pero la base de datos nunca cambiaba —
  bug silencioso, detectado únicamente por los tests de persistencia real
  (`$user->fresh()->google2fa_enabled`), no por inspección visual del
  flujo. Corregido añadiendo ambos campos a `$fillable`.
- Test: `tests/Feature/TwoFactorAuthTest.php` (5 tests: redirección forzada,
  verificación de código correcto/incorrecto, usuario sin 2FA sin desafío,
  persistencia de activación, limpieza de secreto al desactivar).

## Pendiente dentro de F8

- Sentry + scrubbing de logs DSGVO: bloqueado hasta que el cliente cree una
  cuenta/DSN de Sentry.
- Tests de rendimiento k6.
- Tests funcionales E2E con Laravel Dusk.
