Sesiones en Fastify
@keenauth/node/bff añade inicio de sesión a una aplicación Fastify 5. Su servidor es el cliente OAuth y guarda todos los tokens en el servidor. El navegador solo recibe una cookie de sesión cifrada y HttpOnly, y nunca ve un token de acceso, de actualización ni de ID. Este es el patrón backend-for-frontend (BFF). El paquete no importa Fastify, por lo que no añade ninguna dependencia. Todavía no está publicado en npm.
Para proteger una API que recibe tokens de portador, consulte Protección de su API.
Antes de empezar
- En el panel de control, cree una aplicación web en su entorno.
- Elija la autenticación del cliente: un secreto de cliente (
client_secret_basic) oprivate_key_jwtcon un par de claves cuya clave pública registre en la aplicación. Se recomiendaprivate_key_jwt, porque ningún secreto compartido sale de su servidor. - Añada el URI de redirección
https://app.acme.example/auth/callbacky el URI de redirección tras cerrar sesiónhttps://app.acme.example/. - Genere un secreto para la cookie de al menos 32 caracteres, por ejemplo con
openssl rand -base64 32. Guárdelo en su almacén de secretos, no en el repositorio.
Configuración
import Fastify from "fastify";
import { createBff, fastifyRequireAuth, fastifySession } from "@keenauth/node/bff";
const bff = createBff({
issuer: "https://acme.eu.keenauth.app", // your environment's issuer, exactly
clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
clientAuth: { method: "client_secret_basic", clientSecret: process.env.KEENAUTH_CLIENT_SECRET! },
appBaseUrl: "https://app.acme.example",
secret: process.env.KEENAUTH_COOKIE_SECRET!,
});
const app = Fastify();
fastifySession(app, bff); // on the root instance, so it applies to every route
app.get("/orders", { preHandler: fastifyRequireAuth(bff) }, async (request) => {
return `Signed in as ${request.keenauth!.session!.user.email}`;
});
Con private_key_jwt, pase la clave privada en lugar del secreto: clientAuth: { method: "private_key_jwt", privateKey: readFileSync("bff-key.pem", "utf8") }. El SDK firma una nueva aserción de cliente para cada solicitud a los endpoints de token y de PAR. Si la clave está en un KMS o en un HSM, pase clientAuth: { method: "private_key_jwt", assertion }, donde assertion() devuelve una nueva aserción de cliente firmada cada vez. La página de Express describe la aserción y los algoritmos admitidos.
Llame a fastifySession en la instancia raíz. Dentro de un plugin encapsulado, solo se aplica a las rutas de ese plugin.
appBaseUrl es la dirección pública de su aplicación. El SDK construye el URI de redirección y todas las redirecciones a partir de ella, nunca a partir de las cabeceras Host o X-Forwarded-* de la solicitud, por lo que la opción trustProxy no le afecta. El protocolo http simple solo se acepta para localhost.
Las opciones se validan cuando se ejecuta createBff, de modo que un error de configuración detiene la aplicación al arrancar.
Rutas
fastifySession añade tres rutas y lee la sesión en un hook onRequest en todas las demás solicitudes:
| Ruta | Qué hace |
|---|---|
GET /auth/login |
Inicia el inicio de sesión. Parámetros de consulta opcionales: returnTo (a dónde llega el usuario) y organization (iniciar sesión en una organización). |
GET /auth/callback |
Completa el inicio de sesión y establece la cookie de sesión. |
GET o POST /auth/logout |
Cierra la sesión en KeenAuth y regresa a su aplicación. |
El inicio de sesión usa el flujo de código de autorización con PKCE (S256). La solicitud se envía al servidor (PAR) cuando su entorno lo ofrece. En el callback, el SDK comprueba state, iss y el token de ID, incluido su nonce. Cada inicio de sesión correcto crea una sesión nueva, y la anterior se descarta.
returnTo debe ser una ruta de su propia aplicación, como /orders?tab=open. Cualquier otro valor, como //evil.example o una URL completa, se ignora y el usuario llega a /.
Proteger rutas
fastifyRequireAuth(bff) es un preHandler que deja pasar una solicitud solo si tiene una sesión:
- una solicitud de página (
GEToHEAD) sin sesión se redirige a/auth/loginconreturnTo; - una solicitud a
/api/…, o con otro método, recibe401con un documento de problema. Pase{ api: true }para responder401en cualquier ruta.
Para POST, PUT, PATCH, DELETE y otros métodos no seguros, el navegador debe indicar que la solicitud proviene de su aplicación: Sec-Fetch-Site: same-origin, o una cabecera Origin igual al origen de su aplicación. Las demás solicitudes reciben 403. Esto protege contra la falsificación de solicitudes entre sitios, además de SameSite=Lax.
Usar la sesión y llamar a su API
request.keenauth contiene:
session:null, o el usuario que ha iniciado sesión.session.usertiene las notificaciones tipadas del token de ID, comosubject,email,name,orgIdyorgRoles; todas las notificaciones están ensession.user.claims. El objeto de sesión nunca contiene un token.accessToken: un token de acceso válido para llamadas del lado del servidor, onull.
app.get("/api/orders", { preHandler: fastifyRequireAuth(bff) }, async (request, reply) => {
const upstream = await fetch("https://api.acme.example/v1/orders", {
headers: { Authorization: `Bearer ${request.keenauth!.accessToken}` },
});
return reply.code(upstream.status).send(await upstream.json());
});
En TypeScript, declare la propiedad una sola vez:
import type { SessionContext } from "@keenauth/node/bff";
declare module "fastify" {
interface FastifyRequest {
keenauth?: SessionContext | null;
}
}
Cuando el token de acceso caduca en menos de 60 segundos, el SDK lo renueva antes de que se ejecute su manejador. Cada renovación rota el token de actualización y establece la nueva cookie de sesión en la respuesta. Si el servidor rechaza el token de actualización, la sesión termina. Un error temporal del servidor deja la sesión sin cambios.
No envíe el token de acceso al navegador.
Cambiar de organización
bff.switchOrganizationPath("org_01j9…", "/dashboard") devuelve un enlace a /auth/login que vuelve a iniciar sesión al usuario en esa organización. KeenAuth comprueba la pertenencia. Después del inicio de sesión, session.user.orgId contiene la organización.
Cerrar sesión
Envíe un POST a /auth/logout desde un formulario. El SDK:
- elimina la cookie de sesión (y la sesión almacenada, si usa un almacén);
- revoca el token de actualización;
- envía el navegador al endpoint de cierre de sesión de su entorno, que lo devuelve a su aplicación.
Se rechaza una solicitud que el navegador marca como procedente de otro sitio. Los navegadores que no envían Sec-Fetch-Site solo pueden cerrar sesión mediante un POST desde sus propias páginas.
Almacenamiento de sesión
De forma predeterminada, toda la sesión está en la cookie, cifrada y autenticada con AES-256-GCM usando una clave derivada de secret. Una cookie modificada o copiada se rechaza. La cookie se llama __Host-keenauth_session.0 y se establece con Secure, HttpOnly, SameSite=Lax y Path=/. En http://localhost se omiten el prefijo __Host- y Secure. Las sesiones grandes se dividen en hasta tres cookies.
Para guardar los tokens en el servidor, pase un almacén:
import { createBff, type SessionStore } from "@keenauth/node/bff";
const store: SessionStore = {
get: async (id) => JSON.parse((await redis.get(`sess:${id}`)) ?? "null"),
set: async (id, session, ttl) => void (await redis.set(`sess:${id}`, JSON.stringify(session), { EX: ttl })),
delete: async (id) => void (await redis.del(`sess:${id}`)),
};
const bff = createBff({ /* … */ store });
La cookie contiene entonces solo un ID de sesión aleatorio y cifrado. Cada inicio de sesión crea un ID nuevo y elimina el registro anterior. El almacén contiene tokens, así que restrinja el acceso a él. MemorySessionStore es solo para desarrollo y pruebas.
Una sesión dura como máximo 7 días (sessionMaxAge); renovar no la prolonga. Para cambiar el secreto, pase una lista: secret: [newSecret, oldSecret]. Elimine el antiguo después de sessionMaxAge.
Límites actuales
- Los tokens vinculados a DPoP y el cierre de sesión de canal trasero todavía no se gestionan.
- Las renovaciones solo se comparten dentro de un mismo proceso del servidor. Con varias instancias, el ajuste de la aplicación
refresh_reuse_interval_s(5 segundos por defecto) permite que una renovación paralela tenga éxito; si lo establece en0, la segunda renovación termina la sesión.