
# Sesiones de Express

`@keenauth/node/bff` añade el inicio de sesión a una aplicación Express 4 o 5. Su servidor es el cliente OAuth y conserva 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 Express, así que no añade ninguna dependencia. Todavía no se ha publicado en npm.

Para proteger una API que recibe tokens de portador, consulte [Protección de su API](/docs/sdk/protect-api). Para llamar a la API de gestión, consulte el [SDK de gestión para Node.js](/docs/sdk/node).

## Antes de empezar

1. En el panel, cree una aplicación web en su entorno.
2. Elija la autenticación del cliente: un secreto de cliente (`client_secret_basic`) o `private_key_jwt` con un par de claves cuya clave pública registre en la aplicación. Se recomienda `private_key_jwt`, porque ningún secreto compartido sale de su servidor.
3. Añada el URI de redirección `https://app.acme.example/auth/callback` y el URI de redirección tras cerrar sesión `https://app.acme.example/`.
4. 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

```ts
import express from "express";
import { createBff, expressRequireAuth, expressSession } 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 = express();
app.use(expressSession(bff));

app.get("/orders", expressRequireAuth(bff), (req, res) => {
  const { session } = req.keenauth!;
  res.send(`Signed in as ${session!.user.email}`);
});
```

Con `private_key_jwt`, pase la clave privada en lugar del secreto. El SDK firma una nueva aserción de cliente para cada solicitud al endpoint de token y de PAR (`aud` es el emisor, `jti` es aleatorio, válido durante 120 segundos, `kid` es la huella RFC 7638 de la clave). El algoritmo depende de la clave: `RS256` para RSA (o `alg: "PS256"`), `ES256` para P-256 y `EdDSA` para Ed25519.

```ts
import { readFileSync } from "node:fs";

const bff = createBff({
  issuer: "https://acme.eu.keenauth.app",
  clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
  clientAuth: { method: "private_key_jwt", privateKey: readFileSync("bff-key.pem", "utf8") },
  appBaseUrl: "https://app.acme.example",
  secret: process.env.KEENAUTH_COOKIE_SECRET!,
});
```

Si la clave está en un KMS o en un HSM, pase en su lugar `clientAuth: { method: "private_key_jwt", assertion }`, donde `assertion()` devuelve una nueva aserción de cliente firmada cada vez. Pase `privateKey` o `assertion`, pero no ambos.

`appBaseUrl` es la dirección pública de su aplicación. El SDK construye a partir de ella el URI de redirección y todas las redirecciones, nunca a partir de los encabezados `Host` o `X-Forwarded-*` de la solicitud, así que la configuración `trust proxy` 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 iniciarla.

## Rutas

`expressSession` sirve tres rutas y lee la sesión 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 admite. 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 descarta la anterior.

`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

`expressRequireAuth(bff)` deja pasar una solicitud solo si hay una sesión:

- una solicitud de página (`GET` o `HEAD`) sin sesión se redirige a `/auth/login` con `returnTo`;
- una solicitud a `/api/…`, o con otro método, recibe `401` con un documento de problema. Pase `{ api: true }` para responder `401` en cualquier ruta.

Para `POST`, `PUT`, `PATCH`, `DELETE` y otros métodos no seguros, el navegador debe indicar que la solicitud procede de su aplicación: `Sec-Fetch-Site: same-origin`, o un encabezado `Origin` igual al origen de su aplicación. Las demás solicitudes reciben `403`. Esto protege frente a la falsificación de solicitudes entre sitios, además de `SameSite=Lax`.

## Usar la sesión y llamar a su API

`req.keenauth` contiene:

- `session`: `null`, o el usuario que ha iniciado sesión. `session.user` tiene las reclamaciones tipadas del token de ID, como `subject`, `email`, `name`, `orgId` y `orgRoles`; todas las reclamaciones están en `session.user.claims`. El objeto de sesión nunca contiene un token.
- `accessToken`: un token de acceso válido para llamadas del lado del servidor, o `null`.

```ts
app.get("/api/orders", expressRequireAuth(bff), async (req, res) => {
  const upstream = await fetch("https://api.acme.example/v1/orders", {
    headers: { Authorization: `Bearer ${req.keenauth!.accessToken}` },
  });
  res.status(upstream.status).json(await upstream.json());
});
```

En TypeScript, declare la propiedad una sola vez:

```ts
import type { SessionContext } from "@keenauth/node/bff";

declare module "express-serve-static-core" {
  interface Request {
    keenauth?: SessionContext | null;
  }
}
```

Cuando el token de acceso caduca en menos de 60 segundos, el SDK lo renueva antes de que se ejecute su controlador. 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 del usuario en esa organización. KeenAuth comprueba la pertenencia. Tras el 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:

1. elimina la cookie de sesión (y la sesión almacenada, si usa un almacén);
2. revoca el token de actualización;
3. 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 sesiones

De forma predeterminada, la sesión completa 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 conservar los tokens en el servidor, pase un almacén:

```ts
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 identificador de sesión aleatorio y cifrado. Cada inicio de sesión crea un identificador nuevo y elimina el registro anterior. El almacén contiene tokens, así que restrinja su acceso. `MemorySessionStore` solo es para desarrollo y pruebas.

Una sesión dura como máximo 7 días (`sessionMaxAge`); renovarla 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` (por defecto 5 segundos) permite que una renovación paralela se realice correctamente; si lo establece en `0`, la segunda renovación termina la sesión.
