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. Per chiamare l'API di gestione, consulta l'SDK di gestione per Node.js.

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

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.

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:

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:

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:

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:

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