Sessions Express
@keenauth/node/bff ajoute l'authentification à une application Express 4 ou 5. Votre serveur est le client OAuth et conserve tous les jetons côté serveur. Le navigateur reçoit uniquement un cookie de session chiffré et HttpOnly, et ne voit jamais de jeton d'accès, de jeton de rafraîchissement ou de jeton d'identité (ID token). Il s'agit du modèle backend-for-frontend (BFF). Le package n'importe pas Express, il n'ajoute donc aucune dépendance. Il n'est pas encore publié sur npm.
Pour protéger une API qui reçoit des jetons porteurs, consultez Protéger votre API. Pour appeler l'API de gestion, consultez le SDK de gestion Node.js.
Avant de commencer
- Dans le tableau de bord, créez une application web dans votre environnement.
- Choisissez l'authentification du client : un secret client (
client_secret_basic) ouprivate_key_jwtavec une paire de clés dont vous enregistrez la clé publique sur l'application.private_key_jwtest recommandé, car aucun secret partagé ne quitte votre serveur. - Ajoutez l'URI de redirection
https://app.acme.example/auth/callbacket l'URI de redirection après déconnexionhttps://app.acme.example/. - 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 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}`);
});
Avec private_key_jwt, transmettez la clé privée à la place du secret. Le SDK signe une nouvelle assertion client pour chaque requête vers les endpoints de jeton et PAR (aud correspond à l'issuer, jti est aléatoire, valable 120 secondes, kid est l'empreinte RFC 7638 de la clé). L'algorithme dépend de la clé : RS256 pour RSA (ou alg: "PS256"), ES256 pour P-256 et EdDSA pour 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!,
});
Si la clé est conservée dans un KMS ou un HSM, transmettez plutôt clientAuth: { method: "private_key_jwt", assertion }, où assertion() renvoie une nouvelle assertion client signée à chaque appel. Transmettez soit privateKey, soit assertion, mais pas les deux.
appBaseUrl est l'adresse publique de votre application. Le SDK construit l'URI de redirection et toutes les redirections à partir de cette valeur, jamais à partir des en-têtes Host ou X-Forwarded-* de la requête ; le paramètre trust proxy 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
expressSession expose trois routes et lit la session pour toutes les autres requêtes :
| Route | Rôle |
|---|---|
GET /auth/login |
Démarre la connexion. Paramètres de requête facultatifs : returnTo (page d'arrivée de l'utilisateur) 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 de code d'autorisation avec PKCE (S256). La requête est transmise au serveur (PAR) lorsque votre environnement le permet. Lors du callback, le SDK vérifie state, iss et le jeton d'identité, y compris son nonce. Chaque connexion réussie crée une nouvelle session ; 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
expressRequireAuth(bff) ne laisse passer une requête que si une session existe :
- une requête de page (
GETouHEAD) sans session est redirigée vers/auth/loginavecreturnTo; - une requête vers
/api/…, ou avec une autre méthode, reçoit401avec un document de problème. Passez{ api: true }pour répondre401sur tout chemin.
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 vérification protège contre la falsification de requête intersite, en complément de SameSite=Lax.
Utiliser la session et appeler votre API
req.keenauth contient :
session:null, ou l'utilisateur connecté.session.userexpose les revendications typées du jeton d'identité, commesubject,email,name,orgIdetorgRoles; toutes les revendications figurent danssession.user.claims. L'objet de session ne contient jamais de jeton.accessToken: un jeton d'accès valide pour les appels côté serveur, ounull.
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, déclarez la propriété une seule fois :
import type { SessionContext } from "@keenauth/node/bff";
declare module "express-serve-static-core" {
interface Request {
keenauth?: SessionContext | null;
}
}
Lorsque le jeton d'accès expire dans moins de 60 secondes, le SDK le rafraîchit avant l'exécution de votre gestionnaire. Chaque rafraîchissement remplace le jeton de rafraîchissement et définit le nouveau cookie de session dans la réponse. Si le serveur refuse le jeton de rafraîchissement, la session prend fin. Une erreur serveur temporaire 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 :
- supprime le cookie de session (et la session stockée, si vous utilisez un store) ;
- révoque le jeton de rafraîchissement ;
- envoie le navigateur vers l'endpoint de déconnexion de votre environnement, qui le renvoie vers votre application.
Une requête que le navigateur signale comme provenant d'un autre site est refusée. Les navigateurs qui n'envoient pas Sec-Fetch-Site ne peuvent se déconnecter que par un POST depuis vos propres pages.
Stockage des sessions
Par défaut, la session entière est stockée 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 chiffré. Chaque connexion crée un nouvel identifiant et supprime l'enregistrement précédent. Le store contient des jetons : limitez strictement l'accès à celui-ci. MemorySessionStore est réservé au développement et aux tests.
Une session dure au maximum 7 jours (sessionMaxAge) ; le rafraîchissement ne la prolonge pas. Pour changer le secret, transmettez une liste : secret: [newSecret, oldSecret]. Supprimez l'ancien après sessionMaxAge.
Limites actuelles
- Les jetons liés par DPoP et la déconnexion back-channel ne sont pas encore pris en charge.
- Les rafraîchissements ne sont partagés qu'au sein d'un même processus serveur. Avec plusieurs instances, le paramètre d'application
refresh_reuse_interval_s(5 secondes par défaut) permet à un rafraîchissement parallèle de réussir ; si vous le fixez à0, le second rafraîchissement met fin à la session.