Resumen

Una de las advertencias más frecuentes de OWASP es que los problemas de seguridad en aplicaciones web rara vez surgen de ataques sofisticados: la mayoría son producto de implementaciones incompletas o configuraciones deficientes. Exactamente eso ocurre cuando un usuario cierra sesión en una pestaña, pero el front-end en otra ventana sigue mostrando información protegida como si nada hubiera pasado.

¿Por qué una sesión abierta en otra pestaña es un hueco de seguridad?

Imagina que un usuario trabaja desde un café internet. Abre varias pestañas de tu aplicación, termina lo que necesita y cierra sesión desde una de ellas. Si las demás pestañas no reaccionan al cambio de estado, cualquier persona que se siente después frente a ese computador podría operar con la cuenta abierta. El front-end nunca se enteró de que la sesión fue invalidada.

Este escenario es un ejemplo clásico de lo que OWASP clasifica como mala configuración de seguridad (Security Misconfiguration): la autenticación existe, pero la protección de recursos quedó a medias.

¿Cómo detecta useSession los cambios de sesión entre pestañas?

El hook useSession de next-auth ya incluye un mecanismo interno que escucha el foco de la ventana [3:08]. Cada vez que el usuario regresa a una pestaña, useSession envía automáticamente un request al servidor para verificar el estado de la sesión.

Al inspeccionar la pestaña de network en las herramientas de desarrollo, se puede observar que el servidor responde con un objeto vacío cuando la sesión ya no existe [3:30]. Sin embargo, si el código del componente no evalúa esa respuesta, la interfaz permanece intacta y sigue mostrando datos protegidos.

¿Qué cambio se necesita en el componente para reaccionar?

La solución es directa: después de obtener la sesión con useSession, se valida si es null o undefined. Cuando no hay sesión válida, el componente retorna una vista alternativa en lugar del contenido protegido [4:05].

jsx const { data: session } = useSession();

if (!session) { return ( <Layout> <AccessDenied /> </Layout> ); }

El componente AccessDenied muestra un mensaje claro —"tu acceso ha sido denegado"— junto con un botón que redirige al usuario al inicio de sesión [5:15]. Así, el front-end reacciona de forma inmediata al cambio de sesión desde otra pestaña.

¿Qué recursos suelen quedar desprotegidos?

No basta con proteger las páginas principales. Un error muy común es olvidar recursos como:

  • PDFs generados para el usuario.
  • Imágenes privadas o documentos confidenciales.
  • Endpoints de API que devuelven datos sensibles.

Cada recurso derivado de una página que requiere autenticación debe estar igualmente autenticado, tanto en el servidor como en el cliente [6:00].

¿Cómo crear un componente Protected reutilizable?

En bases de código grandes, repetir la lógica de validación de sesión en cada página se vuelve insostenible. La propuesta es crear un componente llamado Protected que encapsule toda esa lógica [6:30].

jsx <Protected loading={<Spinner />} error={<AccessDenied />}

{(session) => ( <WallContent session={session} /> )} </Protected>

Este componente utiliza el patrón de render function: recibe un children que es una función, y solo la ejecuta cuando la sesión es válida. Además, acepta dos props adicionales:

  • loading: lo que se muestra mientras se verifica la sesión en el lado del cliente.
  • error: lo que aparece cuando el usuario pierde su sesión o no está autenticado.

Con este enfoque, cada página protegida se limita a envolver su contenido dentro de <Protected>, delegando por completo la responsabilidad de verificar y reaccionar ante cambios de sesión.

La autenticación sin protección efectiva de recursos es una autenticación incompleta. Si quieres llevar esto un paso más allá, intenta implementar el componente Protected con soporte para loading y error, y comparte tu solución en los comentarios.