La session dans le backend
Comment createTRPCContext injecte la session dans chaque procédure, qui d’autre lit la session (layouts serveur, pages), et comment protectedProcedure en fait des garanties : un membre toujours présent, un identifiant qui vient de la session, jamais de l’entrée.
Précédemment.
getSessionFromRequestvérifie le jeton, relit la session et le membre en base, contrôle l’expiration, et renvoie unSessionClient— ounull.
L’injection : createTRPCContext reprend le relais#
Nous avons construit getSessionFromRequest. Mais cette fonction est inerte tant que personne ne l’appelle.
Qui l’appelle ? createTRPCContext, exactement comme nous l’avons vu dans la partie « API ».
export async function createTRPCContext({ req,}: { req: Request | NextRequest;}): Promise<TRPCContext> { const sessionClient = await getSessionFromRequest(req); return { sessionClient, prisma, requestIp: getIpFromRequest(req), userAgent: req.headers.get("user-agent"), appRoot: APP_ROOT, };}À chaque requête tRPC :
- la route
/api/trpcreçoit la requête fetchRequestHandlerappellecreateContextcreateContextappellecreateTRPCContext({ req })createTRPCContextappellegetSessionFromRequest(req)- le résultat (
SessionClient | null) est placé dansctx.sessionClient
Le flux d’authentification dans le contexte de tRPC#
Scroller horizontalement si nécessaire
À partir de là, toutes les procédures du backend ont accès à ctx.sessionClient. Une procédure publique reçoit null si personne n’est connecté. Une procédure protégée bénéficie du contexte resserré, et lit ctx.sessionClient.user.id sans vérification.
tRPC n’est pas le seul client de getSessionFromRequest. Les pages rendues par le serveur l’appellent aussi, sans requête à lui passer : la fonction lit alors le cookie avec cookies() de Next. C’est le cas du layout racine (chapitre 8), du layout qui garde le tableau de bord, et des pages éditoriales encore en construction (chapitre 9).
C’est ce que nous avons construit dans la partie « API », mais maintenant nous savons précisément d’où vient cette session et comment elle est reconstruite.
👉 Où nous en sommes : la session est injectée partout où le backend en a besoin. Il nous reste à voir comment le code métier exploite cette injection — et pourquoi cela change radicalement sa lisibilité.
Comment protectedProcedure exploite cette session#
Pour boucler la boucle, regardons comment protectedProcedure utilise la session reconstruite.
Rappel, depuis core.ts :
const isAuthed = t.middleware(({ ctx, next }) => { const sessionClient = ctx.sessionClient; if (!sessionClient?.user) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Connexion requise." }); } return next({ ctx: { ...ctx, sessionClient: sessionClient as AuthenticatedSessionClient, } as AuthenticatedTRPCContext, });});export const protectedProcedure = t.procedure.use(isAuthed);Ce middleware fait deux choses :
1) Il bloque les requêtes anonymes.
Si ctx.sessionClient?.user est absent, il lève UNAUTHORIZED (« Connexion requise. »). La procédure ne s’exécute jamais.
2) Il resserre le type du contexte.
Le as AuthenticatedSessionClient n’est pas un cast aveugle : il est justifié par la vérification qui le précède. À partir de ce point, ctx.sessionClient.user n’est plus nullable, et TypeScript le sait.
L’usage concret dans le code métier#
Regardons comment cela se traduit dans une procédure réelle, celle par laquelle un instructeur enregistre sa présentation :
saveMyInstructorBio: protectedProcedure .input( z.object({ // Le même contrat que tout contenu du constructeur de pages : un document invalide est // refusé à l'entrée, au lieu d'être enregistré tel quel. bio: pageContentSchemaV1.nullable(), order: z.number().int().nullable().optional(), }), ) .mutation(async ({ ctx, input }) => { assertOwnAvatars(ctx.sessionClient.user.id, input.bio); const userId = ctx.sessionClient.user.id; // Seul un titulaire a une présentation publique. Un autre membre pourrait sinon référencer des // fichiers, et les rendre servis au public, sans qu'aucune page ne les montre. if (!(await isInstructorUser(ctx.prisma, userId))) { throw new TRPCError({ code: "FORBIDDEN", message: "La présentation est réservée aux instructeurs." }); } if (!ctx.sessionClient.user.isAdmin) { // L'ordre de la page publique est une décision du club : un instructeur ne s'y place pas // lui-même en tête. if (input.order !== undefined) { throw new TRPCError({ code: "FORBIDDEN", message: "L'ordre d'affichage est fixé par un administrateur." }); } if (input.bio !== null) await assertBioMediaAllowed(ctx.prisma, userId, input.bio); } await ctx.prisma.$transaction(async (tx) => { await tx.user.update({ where: { id: userId }, data: { instructorBio: input.bio === null ? Prisma.DbNull : (input.bio as Prisma.InputJsonValue), ...(input.order !== undefined ? { instructorOrder: input.order } : {}), }, }); // Déclare les fichiers de la bibliothèque utilisés par la bio : sans // ce lien, ils ne sont ni protégés, ni servis aux visiteurs. await syncPageMediaReferences(tx, { pageType: "INSTRUCTOR_BIO", pageId: userId, newContent: input.bio, }); }); return { success: true }; }),La confiance justifiée
const userId = ctx.sessionClient.user.id;Pas de ?. Pas de vérification. Pas de TRPCError défensive.
Tout cela est garanti par la chaîne :
getSessionFromRequesta vérifié la signature du jeton, l’existence de la session en base et son expiration ;createTRPCContexta injecté le résultat dansctx.sessionClient;protectedProcedurea vérifié queuserest présent, et a resserré le type.
Le proxy, lui, n’est pas dans cette chaîne : il ne s’exécute que devant les pages du tableau de bord et du profil, jamais devant /api/trpc (chapitre 8). Une procédure ne doit donc jamais compter sur lui.
Quand la procédure écrit ctx.sessionClient.user.id, elle s’appuie sur toute cette chaîne. Et l’identifiant vient de la session, jamais de l’entrée : un membre ne peut pas enregistrer la présentation de quelqu’un d’autre en envoyant un autre identifiant.
C’est ce qu’on appelle écrire du code « confiant » — non pas naïf, mais reposant sur des garanties architecturales explicites.
Une procédure métier n’a aucune logique d’authentification.
Elle exploite les garanties construites en amont : protectedProcedure → contexte resserré → membre toujours présent.
👉 Où nous en sommes : la chaîne de lecture est bouclée. Le cookie arrive, le backend le transforme en identité, et les procédures métier s’en servent sans effort. Reste maintenant à construire le chemin inverse : casser cette chaîne.
À suivre#
La lecture est bouclée. Il faut maintenant regarder l’autre bout de la vie d’une session : comment elle meurt.
Chapitre suivant : fermer une session. La déconnexion et ses deux suppressions, la purge des sessions abandonnées, et le changement de mot de passe qui ferme les autres appareils.