Sessions Fastify

@keenauth/node/bff ajoute la connexion à une application Fastify 5. Votre serveur est le client OAuth et conserve tous les jetons côté serveur. Le navigateur reçoit seulement un cookie de session chiffré et HttpOnly, et ne voit jamais de jeton d'accès, de jeton d'actualisation ni de jeton ID. C'est le modèle backend-for-frontend (BFF). Le package n'importe pas Fastify et n'ajoute donc aucune dépendance. Il n'est pas encore publié sur npm.

Pour protéger une API qui reçoit à la place des jetons bearer, consultez Protéger votre API.

Avant de commencer

  1. Dans le tableau de bord, créez une application web dans votre environnement.
  2. Choisissez l'authentification du client : un secret client (client_secret_basic) ou private_key_jwt avec une paire de clés dont vous enregistrez la clé publique sur l'application. private_key_jwt est recommandé, car aucun secret partagé ne quitte votre serveur.
  3. Ajoutez l'URI de redirection https://app.acme.example/auth/callback et l'URI de redirection après déconnexion https://app.acme.example/.
  4. Générez un secret de cookie d'au moins 32 caractères, par exemple avec openssl rand -base64 32. Conservez-le dans votre coffre de secrets, et non dans le dépôt.

Configuration

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

Avec private_key_jwt, transmettez la clé privée à la place du secret : clientAuth: { method: "private_key_jwt", privateKey: readFileSync("bff-key.pem", "utf8") }. Le SDK signe une nouvelle assertion client pour chaque requête vers les endpoints de jeton et de PAR. Si la clé se trouve dans un KMS ou un HSM, transmettez clientAuth: { method: "private_key_jwt", assertion }, où assertion() renvoie une nouvelle assertion client signée à chaque appel. La page Express décrit l'assertion et les algorithmes pris en charge.

Appelez fastifySession sur l'instance racine. Dans un plugin encapsulé, elle ne s'applique qu'aux routes de ce plugin.

appBaseUrl est l'adresse publique de votre application. Le SDK construit à partir de celle-ci l'URI de redirection et toutes les redirections, jamais à partir des en-têtes Host ou X-Forwarded-* de la requête. L'option trustProxy n'a donc aucun effet sur elle. Le protocole http n'est accepté que pour localhost.

Les options sont vérifiées lors de l'exécution de createBff, de sorte qu'une erreur de configuration arrête l'application au démarrage.

Routes

fastifySession ajoute trois routes et lit la session dans un hook onRequest pour toutes les autres requêtes :

Route Rôle
GET /auth/login Démarre la connexion. Paramètres de requête facultatifs : returnTo (où l'utilisateur arrive) et organization (se connecter à une organisation).
GET /auth/callback Termine la connexion et définit le cookie de session.
GET ou POST /auth/logout Déconnecte l'utilisateur chez KeenAuth et le renvoie vers votre application.

La connexion utilise le flux d'autorisation par code avec PKCE (S256). La requête est envoyée au serveur (PAR) lorsque votre environnement le propose. Au retour, le SDK vérifie state, iss et le jeton ID, y compris son nonce. Chaque connexion réussie crée une nouvelle session, et l'ancienne est supprimée.

returnTo doit être un chemin de votre propre application, comme /orders?tab=open. Toute autre valeur, comme //evil.example ou une URL complète, est ignorée, et l'utilisateur arrive sur /.

Protéger les routes

fastifyRequireAuth(bff) est un preHandler qui laisse passer une requête uniquement si une session existe :

Pour POST, PUT, PATCH, DELETE et les autres méthodes non sûres, le navigateur doit indiquer que la requête provient de votre application : Sec-Fetch-Site: same-origin, ou un en-tête Origin égal à l'origine de votre application. Les autres requêtes reçoivent 403. Cette protection s'ajoute à SameSite=Lax contre les attaques de falsification de requête intersite (CSRF).

Utiliser la session et appeler votre API

request.keenauth contient :

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, déclarez la propriété une seule fois :

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

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

Lorsque le jeton d'accès expire dans moins de 60 secondes, le SDK l'actualise avant l'exécution de votre handler. Chaque actualisation renouvelle le jeton d'actualisation et définit le nouveau cookie de session dans la réponse. Si le serveur refuse le jeton d'actualisation, la session prend fin. Une erreur temporaire du serveur laisse la session inchangée.

N'envoyez jamais le jeton d'accès au navigateur.

Changer d'organisation

bff.switchOrganizationPath("org_01j9…", "/dashboard") renvoie un lien vers /auth/login qui reconnecte l'utilisateur dans cette organisation. KeenAuth vérifie l'appartenance. Après la connexion, session.user.orgId contient l'organisation.

Se déconnecter

Envoyez une requête POST vers /auth/logout depuis un formulaire. Le SDK :

  1. supprime le cookie de session (et l'enregistrement stocké, si vous utilisez un store) ;
  2. révoque le jeton d'actualisation ;
  3. envoie le navigateur vers l'endpoint de déconnexion de votre environnement, qui le renvoie vers votre application.

Une requête que le navigateur marque comme provenant d'un autre site est refusée. Les navigateurs qui n'envoient pas Sec-Fetch-Site ne peuvent se déconnecter qu'avec un POST depuis vos propres pages.

Stockage de session

Par défaut, la session entière est dans le cookie, chiffrée et authentifiée avec AES-256-GCM, à l'aide d'une clé dérivée de secret. Un cookie modifié ou copié est rejeté. Le cookie se nomme __Host-keenauth_session.0 et est défini avec Secure, HttpOnly, SameSite=Lax et Path=/. Sur http://localhost, le préfixe __Host- et Secure sont omis. Les sessions volumineuses sont réparties sur jusqu'à trois cookies.

Pour conserver les jetons côté serveur, transmettez un 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 });

Le cookie ne contient alors qu'un identifiant de session aléatoire et chiffré. Chaque connexion crée un nouvel identifiant et supprime l'enregistrement précédent. Le store contient des jetons : limitez l'accès à celui-ci. MemorySessionStore est réservé au développement et aux tests.

Une session dure au maximum 7 jours (sessionMaxAge) ; l'actualisation ne la prolonge pas. Pour changer le secret, transmettez une liste : secret: [newSecret, oldSecret]. Supprimez l'ancien après sessionMaxAge.

Limites actuelles