Las dos defensas del flujo OAuth
El ida y vuelta con Google tiene dos puntos delicados, y cada uno tiene su defensa. Se confunden con facilidad porque las dos son “listas de cosas permitidas”, pero paran ataques distintos.
| Defensa | Qué impide | Responde a |
|---|---|---|
Cookie oauth_state |
Que alguien te haga completar su login | CSRF |
ALLOWED_REDIRECT_URIS |
Que el flujo devuelva el navegador a un sitio ajeno | Open redirect |
Ninguna de las dos decide quién puede entrar. Eso está en la consola de Google — ver qué es, y qué no es.
La cookie de estado
Al empezar se genera un valor aleatorio de 32 bytes que viaja por dos caminos a la vez: como
parámetro state hacia Google, y como cookie httpOnly en el navegador. En la vuelta tienen que
coincidir:
function isValidOAuthState(req: Request): boolean {
const state = req.query.state as string | undefined;
const savedState = req.cookies[OAUTH_STATE_COOKIE] as string | undefined;
return Boolean(state && savedState && state === savedState);
}
Sin esto, alguien podría inducirte a completar un callback preparado por él y acabar con su sesión en tu navegador.
El detalle que parece una errata: sameSite: "lax"
Lo normal sería pensar que una cookie de seguridad debería ser strict. Aquí rompería el login,
y el código lo explica:
sameSitetiene que serlax(nostrict): la vuelta desde Google es una navegación de nivel superior entre sitios, ystrictimpediría al navegador devolver esta cookie.
Con strict el navegador no manda la cookie al volver de Google, isValidOAuthState falla siempre
y nadie entra nunca. No lo “endurezcas”.
Dura diez minutos: es de un solo uso y para un viaje que tarda segundos.
La allowlist de redirección
Se comprueba dos veces, y la segunda parece redundante:
- En
/auth/google, contra elredirect_urique llega por query. - En el callback, contra el que se recuperó de la cookie.
El propio código reconoce que la cookie solo puede contener un valor que ya pasó el primer filtro, y lo llama una segunda puerta barata por si esa invariante se rompe alguna vez. Es defensa en profundidad con coste cero.
La comparación es de igualdad exacta (includes sobre la lista), no por prefijo ni por dominio.
Es lo correcto —comparar prefijos es de donde salen la mitad de los open redirect— pero significa
que una barra final de más o un http en vez de https no coinciden.
El otro detalle que parece un olvido: sin destino por defecto
Si no hay redirect_uri válido, el flujo muere con un 400 en vez de caer a algún sitio
razonable. Es intencionado: no hay un frontend propio al que volver, y el comentario del código lo
razona — si la cookie falta o no es válida, o expiró la ventana o alguien la manipuló, y en
cualquiera de los dos casos no sabemos a dónde es seguro mandar el navegador.
El orden de las operaciones en el callback
Un detalle fácil de romper al tocar esa función: las dos cookies se borran antes de decidir nada,
salga bien o mal el login. Si solo se limpiaran en el camino del éxito, un intento fallido dejaría
un state vivo durante diez minutos, que es justo lo que la defensa quiere evitar.