
# Sessioni Express

`@keenauth/node/bff` aggiunge l'accesso a un'applicazione Express 4 o 5. Il tuo server è il client OAuth e conserva tutti i token lato server. Il browser riceve solo un cookie di sessione crittografato e HttpOnly e non vede mai un access token, un refresh token o un ID token. Questo è il pattern backend-for-frontend (BFF). Il pacchetto non importa Express, quindi non aggiunge dipendenze. Non è ancora pubblicato su npm.

Per proteggere un'API che riceve bearer token, consulta [Proteggere la tua API](/docs/sdk/protect-api). Per chiamare l'API di gestione, consulta l'[SDK di gestione per Node.js](/docs/sdk/node).

## Prima di iniziare

1. Nella dashboard, crea un'applicazione web nel tuo ambiente.
2. Scegli l'autenticazione del client: un client secret (`client_secret_basic`) oppure `private_key_jwt` con una coppia di chiavi la cui chiave pubblica registri sull'applicazione. Si consiglia `private_key_jwt`, perché nessun segreto condiviso lascia il tuo server.
3. Aggiungi il redirect URI `https://app.acme.example/auth/callback` e il redirect URI di disconnessione `https://app.acme.example/`.
4. Genera un cookie secret di almeno 32 caratteri, ad esempio con `openssl rand -base64 32`. Conservalo nel tuo secret store, non nel repository.

## Configurazione

```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`, passa la chiave privata al posto del segreto. L'SDK firma una nuova client assertion per ogni richiesta agli endpoint di token e PAR (`aud` è l'issuer, `jti` è casuale, valida per 120 secondi, `kid` è il thumbprint RFC 7638 della chiave). L'algoritmo segue la chiave: `RS256` per RSA (o `alg: "PS256"`), `ES256` per P-256 e `EdDSA` per 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!,
});
```

Se la chiave si trova in un KMS o in un HSM, passa invece `clientAuth: { method: "private_key_jwt", assertion }`, dove `assertion()` restituisce una nuova client assertion firmata ogni volta. Passa `privateKey` oppure `assertion`, non entrambi.

`appBaseUrl` è l'indirizzo pubblico della tua app. L'SDK costruisce da qui il redirect URI e tutti i redirect, mai dagli header `Host` o `X-Forwarded-*` della richiesta, quindi l'impostazione `trust proxy` non lo influenza. Il protocollo `http` è accettato solo per `localhost`.

Le opzioni vengono verificate quando viene eseguito `createBff`, quindi un errore di configurazione blocca l'app all'avvio.

## Route

`expressSession` espone tre route e legge la sessione in ogni altra richiesta:

| Route | Cosa fa |
|---|---|
| `GET /auth/login` | Avvia l'accesso. Parametri query facoltativi: `returnTo` (dove atterra l'utente) e `organization` (accedi a un'organizzazione). |
| `GET /auth/callback` | Completa l'accesso e imposta il cookie di sessione. |
| `GET` o `POST /auth/logout` | Disconnette in KeenAuth e torna alla tua app. |

L'accesso usa il flusso authorization code con PKCE (S256). La richiesta viene inviata al server (PAR) quando il tuo ambiente lo supporta. Nel callback l'SDK verifica `state`, `iss` e l'ID token, incluso il suo `nonce`. Ogni accesso riuscito crea una nuova sessione; quella precedente viene eliminata.

`returnTo` deve essere un percorso della tua app, ad esempio `/orders?tab=open`. Qualsiasi altro valore, come `//evil.example` o un URL completo, viene ignorato e l'utente atterra su `/`.

## Proteggere le route

`expressRequireAuth(bff)` lascia passare una richiesta solo con una sessione:

- una richiesta di pagina (`GET` o `HEAD`) senza sessione viene reindirizzata a `/auth/login` con `returnTo`;
- una richiesta a `/api/…`, oppure con un altro metodo, riceve `401` con un documento problem. Passa `{ api: true }` per rispondere `401` su qualsiasi percorso.

Per `POST`, `PUT`, `PATCH`, `DELETE` e gli altri metodi non sicuri, il browser deve dichiarare che la richiesta proviene dalla tua app: `Sec-Fetch-Site: same-origin`, oppure un header `Origin` uguale all'origine della tua app. Le altre richieste ricevono `403`. Questo protegge dal cross-site request forgery oltre a `SameSite=Lax`.

## Usare la sessione e chiamare la tua API

`req.keenauth` contiene:

- `session`: `null`, oppure l'utente autenticato. `session.user` ha claim tipizzati dell'ID token, come `subject`, `email`, `name`, `orgId` e `orgRoles`; tutti i claim sono in `session.user.claims`. L'oggetto sessione non contiene mai un token.
- `accessToken`: un access token valido per chiamate lato server, oppure `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());
});
```

In TypeScript, dichiara la proprietà una sola volta:

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

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

Quando l'access token scade entro 60 secondi, l'SDK lo aggiorna prima che il tuo handler venga eseguito. Ogni aggiornamento ruota il refresh token e imposta il nuovo cookie di sessione nella risposta. Se il server rifiuta il refresh token, la sessione termina. Un errore temporaneo del server lascia la sessione invariata.

Non inviare l'access token al browser.

## Cambiare organizzazione

`bff.switchOrganizationPath("org_01j9…", "/dashboard")` restituisce un link a `/auth/login` che fa accedere di nuovo l'utente in quell'organizzazione. KeenAuth verifica l'appartenenza. Dopo l'accesso, `session.user.orgId` contiene l'organizzazione.

## Disconnettersi

Invia una `POST` a `/auth/logout` da un form. L'SDK:

1. elimina il cookie di sessione (e la sessione memorizzata, se usi uno store);
2. revoca il refresh token;
3. invia il browser all'endpoint di disconnessione del tuo ambiente, che lo riporta alla tua app.

La richiesta che il browser contrassegna come proveniente da un altro sito viene rifiutata. I browser che non inviano `Sec-Fetch-Site` possono disconnettere solo con una `POST` dalle tue pagine.

## Archiviazione della sessione

Di default l'intera sessione si trova nel cookie, crittografata e autenticata con AES-256-GCM usando una chiave derivata da `secret`. Un cookie modificato o copiato viene rifiutato. Il cookie si chiama `__Host-keenauth_session.0` e viene impostato con `Secure`, `HttpOnly`, `SameSite=Lax` e `Path=/`. Su `http://localhost` il prefisso `__Host-` e `Secure` vengono omessi. Le sessioni grandi vengono suddivise in al massimo tre cookie.

Per conservare i token sul server, passa uno store:

```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 });
```

Il cookie contiene quindi solo un ID di sessione casuale crittografato. Ogni accesso crea un nuovo ID ed elimina il record precedente. Lo store contiene i token, quindi limita l'accesso. `MemorySessionStore` serve solo per sviluppo e test.

Una sessione dura al massimo 7 giorni (`sessionMaxAge`); l'aggiornamento non la estende. Per cambiare il secret, passa una lista: `secret: [newSecret, oldSecret]`. Rimuovi quello vecchio dopo `sessionMaxAge`.

## Limiti attuali

- I token vincolati con DPoP e il back-channel logout non sono ancora gestiti.
- Gli aggiornamenti sono condivisi solo all'interno di un singolo processo server. Con più istanze, l'impostazione dell'applicazione `refresh_reuse_interval_s` (default 5 secondi) consente a un aggiornamento parallelo di riuscire; se la imposti a `0`, il secondo aggiornamento termina la sessione.
