Durante años, servir tu SPA como archivos estáticos en S3 detrás de CloudFront fue —y sigue siendo— una recomendación popular: es barato, escala solo y no pagas cómputo. Pero esa arquitectura arrastra un problema de seguridad que mucha gente no ve: al no haber un backend propio, el token de autenticación, la URL de tu proveedor de identidad y la de tu API terminan expuestos en el navegador. Veamos por qué pasa y qué recomienda AWS para arreglarlo.
Por qué llegamos aquí
La lógica era impecable en su momento: si tu frontend es una SPA (React, Angular, Vue), el navegador solo necesita descargar HTML, CSS y JavaScript. ¿Para qué pagar un servidor encendido las 24 horas? Subes el build a un bucket S3, pones CloudFront delante para caché y HTTPS global, y listo: hosting casi gratis, sin servidores que mantener.
El efecto secundario: ya no tienes ningún componente de servidor propioen la ruta del usuario. Y como la autenticación hay que hacerla en algún lado, se hace donde sí hay código corriendo: el navegador. Ahí empieza el problema.
💡 No es que S3+CloudFront sea inseguro por sí mismo
Las tres exposiciones
Cuando la SPA es la que autentica (típicamente con Cognito Hosted UI o Amplify como public client), ocurren tres exposiciones simultáneas:
- 1. Exposición del token: el
access_tokeny elrefresh_tokense guardan enlocalStorage/memoria. Un XSS —en tu código o en una dependencia npm comprometida— los lee y los exfiltra. Con elrefresh_token, el atacante mantiene acceso prolongado. - 2. Exposición de la URL del proveedor de identidad: el navegador habla directo con Cognito/tu IdP, así que su dominio,
client_id, scopes y parámetros están a la vista en las DevTools. - 3. Exposición de la URL del backend: la SPA llama directamente a tu API, revelando su dominio, rutas y contratos. Un atacante mapea tu infraestructura solo mirando el tráfico de red.
🚫 Y no, el WAF no lo arregla
La solución: reintroducir una capa server-side
El patrón de fondo es el Backend For Frontend: que un componente de servidor —no el navegador— sea el cliente OAuth confidencial, guarde los tokens y exponga al browser solo una cookie de sesión HttpOnly. La buena noticia: AWS te da dos formas gestionadas de hacerlo sin renunciar del todo a la comodidad del hosting estático.
Opción A — ALB con authenticate-oidc
Si tu tráfico puede pasar por un Application Load Balancer, el propio ALB hace la autenticación OIDC por ti. Es un BFF gestionado sin escribir código de tokens.
- Configuras una regla de listener
authenticate-oidc(oauthenticate-cognito). El ALB ejecuta el flujo Authorization Code contra Cognito/tu IdP. - El ALB guarda los tokens y entrega al navegador la cookie
AWSELBAuthSessionCookie(HttpOnly). El token no llega al browser. - A tu aplicación le inyecta la identidad vía headers firmados:
x-amzn-oidc-data(claims como JWT),x-amzn-oidc-accesstokenyx-amzn-oidc-identity. Tu backend solo valida esos headers.
⚠️ Cuándo NO encaja
Opción B — CloudFront + Lambda@Edge (Authorization@Edge)
Este es el patrón que AWS documenta específicamente para proteger contenido estático en S3+CloudFront. Una función Lambda@Edge se ejecuta en el borde y actúa como BFF: maneja el flujo OIDC con Cognito, valida la sesión en cada request y transfiere el JWT mediante cookies HttpOnly, de forma transparente para la SPA.
- La Lambda@Edge intercepta las requests en CloudFront (viewer/origin request), verifica la cookie de sesión y, si falta o expiró, redirige al login de Cognito.
- Los JWT se transfieren en cookies HttpOnly, no en headers que la SPA tenga que manejar. Así el JavaScript del navegador nunca toca el token.
- Los assets de S3 solo se sirven a usuarios autenticados, y la URL del IdP/backend queda detrás del edge.
✅ No lo escribas desde cero
aws-samples/cloudfront-authorization-at-edge, desplegable como aplicación del Serverless Application Repository. Te da el flujo Cognito + cookies HttpOnly + rotación de tokens ya resuelto.El rol real de Cognito
Aquí el matiz que confunde a mucha gente: Cognito no resuelve por sí solo la exposición del token. Cognito es el proveedor de identidad; lo que decide tu seguridad es dónde pones el cliente confidencial.
- Cognito directo desde la SPA (Hosted UI/Amplify como public client) → el token vuelve al navegador → misma vulnerabilidad.
- Cognito detrás de un ALB o Lambda@Edge → el token se queda server-side → problema resuelto.
Resumen de opciones
Postura correcta: defensa en profundidad, no «una sola bala».
| Mecanismo | Qué hace con el token | ¿Quita el token del navegador? |
|---|---|---|
| AWS WAF | Filtra tráfico entrante | No |
| CSP estricta | Dificulta ejecutar/exfiltrar XSS | Parcial |
| ALB authenticate-oidc | Token en el ALB, cookie al browser | Sí |
| CloudFront + Lambda@Edge | Token en el borde, cookie HttpOnly | Sí |
| Cognito (public client en SPA) | Token al navegador | No |
En resumen
Servir tu SPA desde S3+CloudFront no es un error, pero deja un hueco: sin backend propio, el token y tus endpoints internos acaban en el navegador. El WAF ayuda con el perímetro, pero no cierra esa clase de ataque. La solución que recomienda AWS es reintroducir una capa server-side que maneje el token —ALB con authenticate-oidc si tienes origen dinámico, o Lambda@Edge (Authorization@Edge) para hosting puramente estático— dejando al navegador solo con una cookie HttpOnly. Cognito es tu IdP; la seguridad la define dónde vive el cliente confidencial.