Authentification
@bext-stack/framework/auth est le noyau réutilisable du flux d'authentification qu'environ 31 sites bext copient actuellement dans leur propre oidc.ts : analyse de cookies, un cookie de session inviolable signé HMAC et OAuth/OIDC PKCE Aides. C'est la moitié de l'authentification ; Autorisation ( authz ) est la moitié « peuvent-ils le faire ».
Le crypto est pur JavaScript — HMAC-SHA256, SHA-256 et base64url sont implémentés dans le module, validés par rapport aux vecteurs canoniques HMAC et RFC 7636 PKCE — il fonctionne donc de manière identique sur V8 et QuickJS avec non node:crypto ou dépendance WebCrypto (les copies mélangent les deux, ce qui se brise en un pur isolat).
Quand l'utiliser#
- Un cookie de session signé pour "qui est cette demande" - sans externe magasin de sessions ou une dépendance cryptographique.
- Lancement d'une connexion OAuth2/OIDC (PKCE + URL d'autorisation).
- Partout où vous écriviez à la main
parseCookies/ un signataire HMAC.
Séances signées#
import { createSession } from "@bext-stack/framework/auth";
const session = createSession<{ userId: string; role: string }>({
secret: process.env.SESSION_SECRET!, // read from env / a secrets store
cookie: "sid",
maxAgeSecs: 86400, // expiry is signed INTO the payload
});
// loader — read + verify (tampered or expired → null):
const s = session.read(request); // { userId, role } | null
// action — issue / clear:
return new Response(null, { status: 303, headers: {
location: "/",
"set-cookie": session.cookie({ userId, role }), // signed Set-Cookie
}});
// logout: session.clearCookie()
Le jeton est base64url(json).signature ; read / verify recalcule le HMAC (temps constant), rejette une charge utile falsifiée et applique le exp intégré à la charge utile — de sorte qu'un jeton copié hors du réseau expire toujours côté serveur, pas seulement dans le Max-Age du navigateur.
| Membre | Fait |
|---|---|
createSession<T>({ secret, cookie?, maxAgeSecs?, sameSite?, secure? }) |
créer un assistant de session |
.read(request) |
vérifier le cookie → T | null |
.issue(data) / .verify(token) |
jeton ⟷ charge utile |
.cookie(data) / .clearCookie() |
Valeurs d'en-tête Set-Cookie |
Cookies et signataire#
import { parseCookies, serializeCookie, createSigner } from "@bext-stack/framework/auth";
parseCookies(request.headers.get("cookie")); // { name: value, ... }
serializeCookie("flash", "hi", { maxAge: 60 }); // "flash=hi; Path=/; Max-Age=60; SameSite=Lax; HttpOnly; Secure"
const signer = createSigner(secret); // low-level HMAC
signer.verify(payload, signer.sign(payload)); // true (constant-time)
OAuth/OIDC (PKCE)#
Assistants indépendants du fournisseur pour le code d'autorisation + flux PKCE :
import { pkceChallenge, buildAuthorizeUrl, randomToken } from "@bext-stack/framework/auth";
const { verifier, challenge } = pkceChallenge(); // stash `verifier` in the session
const state = randomToken();
const url = buildAuthorizeUrl({
authorizationEndpoint: "https://idp/authorize",
clientId, redirectUri, scope: "openid email",
state, codeChallenge: challenge, // adds code_challenge_method=S256
});
// redirect the user to `url`; on callback, exchange the code + verifier at the token endpoint.
pkceChallengeFromVerifier(v) et les assistants base64urlEncode / base64urlDecode sont exportés pour créer vous-même l'étape d'échange de jetons.
Stockez les verifier et state dans un cookie signé de courte durée (utilisez
createSession ) lors de la redirection et vérifiez state lors du rappel : il s'agit de la défense CSRF pour le flux de connexion.
Le client OIDC complet ( createOidcClient )#
Les aides ci-dessus sont les pièces. createOidcClient représente l'ensemble du flux de code d'autorisation (PKCE public) dans un objet configuré — la forme exacte d'environ 30 sites bext utilisés pour copier-coller sous la forme d'un lib/oidc.ts d'environ 500 lignes. Vous lui donnez la configuration ; il restitue PKCE, l'URL d'autorisation, l'échange de jetons, le décodage du jeton d'identification, une enveloppe de flux à cookie unique signée (vérificateur PKCE + état CSRF + chemin de retour dans un cookie signé HMAC), une session signée HMAC et la porte de routage.
import { createOidcClient } from "@bext-stack/framework/oidc";
const client = createOidcClient({
issuer: "https://auth.example.com", // default: the bext IdP
clientId: "my-app",
redirectUri: "https://my-app.com/auth/callback",
sessionSecret: () => process.env.SESSION_SECRET, // string OR lazy resolver
sessionCookieName: "my_session",
flowCookieName: "my_flow",
flowCarrier: "ret", // "ret" = 303 back to a path; "popup" = postMessage
});
Coup d'envoi de la connexion : un cookie signé transporte l'intégralité du flux à travers la redirection :
export async function action({ request }) {
const verifier = client.generateCodeVerifier();
const challenge = await client.generateCodeChallenge(verifier);
const state = client.generateState(), nonce = client.generateNonce();
const flow = client.buildFlowState({ pkce: verifier, state, nonce, ret: "/" });
return new Response(null, { status: 303, headers: {
Location: client.buildAuthorizeUrl({ codeChallenge: challenge, state, nonce }),
"set-cookie": client.flowEnvelopeCookie(flow),
}});
}
Rappel : vérifiez l'état, échangez le code avec le vérificateur PKCE, signez la session :
export async function loader({ request }) {
const flow = client.readFlowEnvelope(request);
const url = new URL(request.url);
if (!flow || flow.state !== url.searchParams.get("state")) throw fail("state");
const tok = await client.exchangeCode({ code: url.searchParams.get("code"), codeVerifier: flow.pkce });
const c = client.decodeIdToken(tok.id_token);
const now = Math.floor(Date.now() / 1000);
const session = client.packSession({ sub: c.sub, name: c.name, email: c.email, iat: now, exp: now + 600 });
const headers = new Headers({ Location: flow.ret });
headers.append("Set-Cookie", client.sessionCookie(session));
headers.append("Set-Cookie", client.clearFlowEnvelope());
throw new Response(null, { status: 303, headers });
}
Lire + portail n'importe quel itinéraire :
const session = client.readSession(request); // verified payload | null
const gated = client.requireSession(request); // session | 303 to /auth/login?return=…
if (gated instanceof Response) return gated;
createOidcClient est compatible octet pour octet avec les copies manuelles qu’il remplace : même base64url, même enveloppe HMAC portant sur la charge base64url, mêmes attributs des cookies. Le cookie de session est SameSite=Lax; Secure, pas HttpOnly, afin de pouvoir lire le nom d’affichage côté client ; le cookie de flux est HttpOnly; SameSite=None. Un site migré continue de valider les cookies actifs déjà émis, comme vérifié dans
oidc.test.ts face à une copie intégrée de l’algorithme exact des sites.
Migration d'un oidc.ts copié-collé#
Remplacez le corps du fichier par une fine cale de réexportation afin que les centaines de
Les sites d'appel import … from "../lib/oidc" ne changent jamais :
// src/lib/oidc.ts
import { createOidcClient } from "@bext-stack/framework/oidc";
const client = createOidcClient<Session>({ /* this site's config */ });
export const {
buildAuthorizeUrl, exchangeCode, decodeIdToken,
packSession, unpackSession, readSession, sessionCookie, clearSessionCookie,
buildFlowState, flowEnvelopeCookie, clearFlowEnvelope, readFlowEnvelope,
requireSession, requireSessionJson, sanitizeReturnPath, parseCookies,
} = client;
export const SESSION_COOKIE_NAME = client.SESSION_COOKIE_NAME;
export interface Session { sub: string; email: string; /* … */ exp: number }
Transmettre sessionSecret en tant que fonction lorsque le secret est lu à partir d'un site par site
[env] qui se résout paresseusement sur un travailleur à froid : une chaîne capturée se bloquerait
undefined. Le résolveur s'exécute à chaque signature/vérification.
Relation avec les capacités d'authentification/session#
Les fonctionnalités Rust Authentification et Session sont des coutures côté serveur. Ce module est le chemin isolé TypeScript qu'une route PRISM utilise directement - le cookie signé réside dans votre code, sans fournisseur à configurer ni dépendance cryptographique à expédier.
Essayez-le#
Le démo de session signée en direct vous connecte avec un cookie inviolable et le vérifie à chaque demande – modifiez le cookie dans DevTools et regardez la session devenir anonyme. Source dans
sites/demo/src/app/examples/session/page.tsx.
Le Démo OAuth/OIDC exécute le plein
Flux createOidcClient — PKCE, enveloppe de flux signée, échange de jetons, session signée — par rapport à un IdP fictif sur le même site. Source dans
sites/demo/src/app/examples/oauth-mock/.
Voir aussi#
- Autorisation — la moitié « peuvent-ils le faire » (
authz). - Authentification / Session — les capacités de Rust.
- Boîte à outils de candidature — le reste de la couche d'application TypeScript.