Sessions
Le point d'entrée @bext-stack/framework/auth fournit createSession : une session tenue
entièrement dans un cookie signé (HMAC-SHA256) et daté. Le serveur ne stocke rien : le
cookie porte les données, leur date d'émission et leur date d'expiration, et la signature empêche
de les modifier. La signature est calculée en JavaScript pur, sans node:crypto ni WebCrypto, et
fonctionne donc dans tous les loaders, actions et routes.
Créer une session
Déclarez la session une fois dans un module partagé, puis importez-la dans vos routes :
// src/lib/session.ts
import { createSession } from "@bext-stack/framework/auth";
export const session = createSession<{ userId: string; role: string }>({
secret: process.env.SESSION_SECRET!,
cookie: "myapp_session", // default "bext_session"
maxAgeSecs: 7 * 24 * 3600, // default 86400 (1 day)
});
| Option | Défaut | Rôle |
|---|---|---|
secret |
(obligatoire) | Clé de signature HMAC-SHA256 |
cookie |
"bext_session" |
Nom du cookie |
maxAgeSecs |
86400 (1 jour) |
Durée de validité, inscrite dans les données signées et reprise comme Max-Age du cookie |
sameSite |
"Lax" |
"Lax", "Strict" ou "None" |
secure |
true |
Passez false pour retirer l'attribut Secure |
path |
"/" |
Attribut Path du cookie |
Le cookie est toujours HttpOnly : le JavaScript de la page ne peut pas le lire. Le secret se lit
dans process.env (voir Secrets) : ne l'écrivez jamais dans le code.
API
createSession<T>(options) retourne un objet Session<T> :
| Membre | Description |
|---|---|
read(request) |
Lit le cookie de la requête, vérifie la signature et l'expiration. Retourne les données, ou null si le cookie est absent, modifié ou expiré. |
cookie(data) |
Retourne la valeur d'un en-tête Set-Cookie qui ouvre la session avec data. |
clearCookie() |
Retourne la valeur d'un en-tête Set-Cookie qui efface le cookie (Max-Age=0). |
issue(data) |
Retourne le jeton signé seul, sans cookie. |
verify(token) |
Vérifie un jeton produit par issue. Retourne les données ou null. |
cookieName |
Le nom du cookie utilisé. |
read et verify retournent les données telles que vous les avez passées : les champs iat et
exp ajoutés à l'émission sont retirés.
Exemple : connexion et déconnexion
Une page de connexion dont l'action vérifie le mot de passe, puis ouvre la session :
// src/app/login/page.tsx
import { password } from "@bext-stack/framework/password";
import { session } from "@/lib/session";
export const dynamic = "force-dynamic";
export function loader({ request }) {
// Already signed in: skip the form.
if (session.read(request)) {
throw new Response(null, { status: 303, headers: { Location: "/compte" } });
}
return {};
}
export async function action({ request }) {
const form = await request.formData();
const email = String(form.get("email") ?? "");
const user = await findUserByEmail(email); // your own lookup
const ok = user && (await password.verify(String(form.get("password") ?? ""), user.passwordHash));
if (!ok) return { error: "Email ou mot de passe incorrect." };
return new Response(null, {
status: 303,
headers: {
Location: "/compte",
"Set-Cookie": session.cookie({ userId: user.id, role: user.role }),
},
});
}
export default function Page({ actionData }) {
return (
<form method="post">
{actionData?.error && <p>{actionData.error}</p>}
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Se connecter</button>
</form>
);
}
Une page protégée lit la session dans son loader et gère la déconnexion dans son action :
// src/app/compte/page.tsx
import { session } from "@/lib/session";
export const dynamic = "force-dynamic";
export function loader({ request }) {
const user = session.read(request);
if (!user) throw new Response(null, { status: 303, headers: { Location: "/login" } });
return { user };
}
export function action() {
// Logout: clear the cookie and go back to the login page.
return new Response(null, {
status: 303,
headers: { Location: "/login", "Set-Cookie": session.clearCookie() },
});
}
export default function Page({ data }) {
return (
<form method="post">
<p>Connecté : {data.user.userId}</p>
<button type="submit">Se déconnecter</button>
</form>
);
}
Le composant de page ne voit pas request : lisez toujours la session dans le loader ou l'action,
et passez à la page ce qu'elle doit afficher (voir Loaders & Actions).
Expiration
- L'expiration est fixe :
expvaut l'heure d'émission plusmaxAgeSecs. Lire la session ne la prolonge pas. Pour la prolonger, renvoyez un nouveausession.cookie(data)dans une réponse, par exemple à la fin d'une action. - Elle est vérifiée côté serveur : un jeton copié depuis le navigateur et rejoué après
expest refusé, même si le cookie n'a pas été effacé. - Changer
maxAgeSecsne touche que les sessions émises ensuite.
À savoir
- Signé, pas chiffré. Les données sont du JSON encodé en base64url, lisible par quiconque a le cookie. N'y mettez qu'un identifiant et ce qui sert à décider vite (un rôle, une langue), jamais un mot de passe, un jeton tiers ou des données personnelles. Gardez-les courtes : elles voyagent dans chaque requête.
- Pas de révocation côté serveur.
clearCookie()efface le cookie du navigateur, mais un jeton copié reste valide jusqu'à sonexp. Changersecretinvalide toutes les sessions d'un coup. Pour déconnecter un seul utilisateur, stockez un numéro de version dans votre base, mettez-le dans la session et comparez-le dans le loader. - Un secret long et aléatoire. Toute personne qui connaît le secret peut fabriquer une session valide.
- Données JSON. Les données passent par
JSON.stringify: uneDaterevient sous forme de chaîne. N'utilisez pasiatniexpcomme noms de champs, ils sont réservés. - Type des données.
Tdoit être un objet à clés chaînes. Un alias de type ou un type littéral ({ userId: string }) convient ; uneinterfacedoit étendreRecord<string, unknown>. - Nom du cookie et cache. En mode ISR, les requêtes qui portent un cookie de session ne passent
pas par le cache de pages. Un nom qui se termine par
session(commebext_sessionoumyapp_session) est reconnu comme tel ; avec un autre nom, ou par prudence, ajoutezexport const dynamic = "force-dynamic"aux pages qui dépendent de la session (voir Routage & Rendu). - Mêmes réglages pour effacer.
clearCookie()reprendcookie,path,sameSiteetsecurede la session : utilisez le même objetsessionpour ouvrir et fermer. SameSite=NoneexigeSecuredans les navigateurs : ne combinez passameSite: "None"avecsecure: false.
Autres utilitaires
Le même point d'entrée exporte les briques utilisées par createSession :
| Fonction | Rôle |
|---|---|
parseCookies(header) |
Transforme un en-tête Cookie en objet { nom: valeur } (valeurs décodées). |
serializeCookie(name, value, options?) |
Construit une valeur Set-Cookie. Options : path ("/"), maxAge, domain, sameSite ("Lax"), secure (true), httpOnly (true). |
createSigner(secret) |
Retourne { sign(payload), verify(payload, signature) } en HMAC-SHA256, signature en base64url. |
timingSafeEqual(a, b) |
Compare deux chaînes en temps constant. |
serializeCookie est utile pour un cookie qui n'est pas une session (préférence, panier) ou pour
fixer domain, que createSession ne propose pas.
Et ensuite
| Page | Ce qu'elle couvre |
|---|---|
| Loaders & Actions | Où lire la requête, redirections par Response |
| Secrets | Fournir SESSION_SECRET à votre application |
| Routage & Rendu | Cache de pages, force-dynamic |