Cómo proteger un panel con él
Todo el sentido de este servidor es que haya más de un consumidor. Hoy solo hay uno —el monitor
de colas de notification-api— así que esta página describe lo que ese hace, que es la plantilla
para el siguiente.
Son cuatro piezas.
1 · El secreto compartido
IDENTITY_JWT_SECRET tiene que valer exactamente lo mismo en los dos servicios. Se llama igual
en ambos, así que buscar el nombre encuentra su pareja. Es lo único que necesita el consumidor para
verificar una sesión: no hace ninguna llamada de vuelta al identity server.
Eso tiene una consecuencia buena y una mala. La buena: un panel sigue aceptando sesiones ya emitidas aunque el identity server esté caído. La mala: no hay revocación. Un token válido lo es hasta que expira, pasen los 15 minutos.
2 · La puerta
Un middleware delante de la ruta protegida. Si no hay sesión, en vez de devolver 401 redirige al login, diciendo a dónde volver:
const redirectUri = `${env.NOTIFICATIONS_BASE_URL}/admin/auth/callback`;
res.redirect(
`${identityUrl}/auth/google?redirect_uri=${encodeURIComponent(redirectUri)}`,
);
Ese redirect_uri tiene que estar en la allowlist del identity server, y la coincidencia es
exacta: sobra una barra final y el flujo muere con un 400.
3 · El callback: verificar otra vez
El consumidor recibe ?token= y no se fía:
try {
jwt.verify(token, env.IDENTITY_JWT_SECRET);
} catch {
return res.status(401).send("Invalid or expired session token");
}
El comentario del código lo llama defensa en profundidad: el token ya trae la firma del identity server, pero nunca se guarda un token sacado de la query sin comprobarlo.
Verificado, se cambia por una cookie propia del consumidor:
res.cookie(ADMIN_SESSION_COOKIE, token, {
httpOnly: true,
secure: isProd,
sameSite: "lax",
maxAge: 15 * 60 * 1000, // igual que el TTL del access token del identity server
});
Ese maxAge debe coincidir con la vida del access token. Si la cookie durase más, el usuario
seguiría pareciendo autenticado con un token ya caducado y cada petición fallaría de forma rara en
vez de mandarle a loguearse otra vez.
4 · El respaldo, que no es opcional
notification-api mantiene dos vías de entrada independientes: la sesión de Google y una
contraseña por Basic Auth.
if (hasValidAdminSession(req, env) || hasValidBasicAuth(req, env)) return next();
El motivo está escrito en el propio fichero: el identity server es una instancia única y sin alta disponibilidad, y perder el acceso al monitor de colas justo cuando eso está caído sería un incidente en sí mismo.
Al montar un consumidor nuevo, deja un camino alternativo. No es desconfianza del diseño: es que este servicio no se puede replicar, así que su caída es total.
La comparación de la contraseña usa timingSafeEqual, no ===.
Checklist para un consumidor nuevo
-
IDENTITY_JWT_SECRETidéntico al del identity server - Su
…/auth/callbackañadido aALLOWED_REDIRECT_URIS, exacto - Middleware que redirige al login en vez de devolver 401
- Callback que verifica el token antes de guardarlo
- Cookie propia,
httpOnly, conmaxAgeigual al TTL del access token (15 min) - Una segunda vía de entrada para cuando el identity server no esté