
# Sessioni Fastify

`@keenauth/node/bff` aggiunge l'accesso a un'applicazione Fastify 5. Il tuo server è il client OAuth e conserva tutti i token lato server. Il browser riceve solo un cookie di sessione cifrato 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 Fastify, quindi non aggiunge dipendenze. Non è ancora pubblicato su npm.

Per proteggere invece un'API che riceve bearer token, vedi [Proteggere la tua API](/docs/sdk/protect-api).

## 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. `private_key_jwt` è consigliato, perché nessun segreto condiviso esce dal tuo server.
3. Aggiungi il redirect URI `https://app.acme.example/auth/callback` e il post-logout redirect URI `https://app.acme.example/`.
4. Genera un cookie secret di almeno 32 caratteri, per esempio con `openssl rand -base64 32`. Conservalo nel tuo secret store, non nel repository.

## Configurazione

```ts
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`, passa la chiave privata al posto del secret: `clientAuth: { method: "private_key_jwt", privateKey: readFileSync("bff-key.pem", "utf8") }`. L'SDK firma un nuovo client assertion per ogni richiesta agli endpoint di token e PAR. Se la chiave si trova in un KMS o in un HSM, passa `clientAuth: { method: "private_key_jwt", assertion }`, dove `assertion()` restituisce un nuovo client assertion firmato ogni volta. La [pagina Express](/docs/sdk/express) descrive l'assertion e gli algoritmi supportati.

Chiama `fastifySession` sull'istanza root. All'interno di un plugin incapsulato si applica solo alle route di quel plugin.

`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'opzione `trustProxy` non ha effetto su di essi. 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

`fastifySession` aggiunge tre route e legge la sessione in un hook `onRequest` su ogni altra richiesta:

| Route | Cosa fa |
|---|---|
| `GET /auth/login` | Avvia l'accesso. Parametri query opzionali: `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 da KeenAuth e torna alla tua app. |

L'accesso usa il flusso authorization code con PKCE (S256). La richiesta viene inviata al server tramite 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 e quella precedente viene eliminata.

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

## Proteggere le route

`fastifyRequireAuth(bff)` è un `preHandler` che 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 indicare che la richiesta arriva 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 dalle richieste cross-site forgery in aggiunta a `SameSite=Lax`.

## Usare la sessione e chiamare la tua API

`request.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", { 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());
});
```

In TypeScript, dichiara la proprietà una sola volta:

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

declare module "fastify" {
  interface FastifyRequest {
    keenauth?: SessionContext | null;
  }
}
```

Quando l'access token scade entro 60 secondi, l'SDK lo rinnova prima che venga eseguito il tuo handler. Ogni rinnovo 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 riaccedere l'utente in quell'organizzazione. KeenAuth verifica l'appartenenza. Dopo l'accesso, `session.user.orgId` contiene l'organizzazione.

## Disconnettersi

Invia una richiesta `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. porta il browser all'endpoint di logout del tuo ambiente, che lo riporta alla tua app.

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

## Archiviazione della sessione

Di default l'intera sessione si trova nel cookie, cifrata 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 fino a 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 e cifrato. Ogni accesso crea un nuovo ID ed elimina il record precedente. Lo store contiene i token, quindi limitane l'accesso. `MemorySessionStore` è solo per sviluppo e test.

Una sessione dura al massimo 7 giorni (`sessionMaxAge`); il rinnovo 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.
- I rinnovi sono condivisi solo all'interno di un singolo processo server. Con più istanze, l'impostazione dell'applicazione `refresh_reuse_interval_s` (default 5 secondi) permette a un rinnovo parallelo di riuscire; se la imposti a `0`, il secondo rinnovo termina la sessione.
