Comment créer une route protégée
Apprends à sécuriser tes routes dans React Router 7 en gérant l’authentification côté serveur et en évitant les failles de sécurité.
Pourquoi protéger une route ?
Toutes les pages ne se valent pas :
- un tableau de bord, - un formulaire de paiement, - des paramètres de compte ...
Ces écrans doivent être accessibles uniquement aux utilisateurs authentifiés.
Dans React Router 7 tu peux tester la session directement dans chaque route ;
mais recopier le même if (!user) redirect(...) devient vite pénible et risqué :
la première omission est une faille de sécurité.
Dans cette leçon tu vas :
- détecter l’utilisateur depuis le cookie de session ;
- transformer ce test en helper réutilisable ;
- l’appeler aussi bien dans les
loaderque dans lesaction; - comprendre le flux complet : redirection, affichage et gestion d’erreur.
Lire la session côté serveur
Le module précédent a posé le cookie __session avec l’userId à la connexion.
Créons un utilitaire pour le récupérer :
1import { createCookieSessionStorage } from "react-router"2import { prisma } from "./db.server"34type SessionData = { userId: string }56const { getSession, commitSession, destroySession } =7createCookieSessionStorage<SessionData>({8cookie: {9name: "__session",10sameSite: "lax",11httpOnly: true,12path: "/",13secrets: [process.env.SESSION_SECRET!], // ➜ prod only14secure: process.env.NODE_ENV === "production",15},16})1718export async function getOptionalUser({ request }: { request: Request }) {19const session = await getSession(request.headers.get("Cookie"))20const id = session.get("userId") as string | undefined21if (!id) return null2223// vérif anti-forgery : l’id existe *vraiment* en base24return prisma.user.findUnique({25where: { id: Number(id) },26select: { id: true, firstName: true, lastName: true },27})28}
getOptionalUser renvoie soit l’utilisateur, soit null.
C’est idéal pour afficher un bouton Login lorsque personne n’est connecté.
Créer requireUser : la garde de sécurité
Le profil, la facturation, l’administration… doivent être fermés : pas d’utilisateur ⇒ pas d’accès. On factorise ce comportement dans une seule fonction :
1import { redirect } from "react-router"23export async function requireUser({ request }: { request: Request }) {4const user = await getOptionalUser({ request })5if (!user) {6throw redirect("/login") // ⬅️ stoppe le loader et redirige7}8return user9}
Remarque :
redirect() lève déjà une Response HTTP 302, React Router capture
l’exception et envoie le bon header au navigateur – aucun composant
d’erreur n’est rendu.
L’utiliser dans un loader
1import type { LoaderFunctionArgs } from "react-router"2import { requireUser } from "~/server/sessions.server"34export async function loader({ request }: LoaderFunctionArgs) {5// 🔒 bloque d’emblée les visiteurs6const user = await requireUser({ request })7return { user }8}
Visiteur anonyme ? Il est téléporté sur /login.
Utilisateur connecté ? Le loader continue et fournit le profil.
…et dans une action
Les mutations POST/PUT/DELETE doivent aussi être protégées :
1import type { ActionFunctionArgs } from "react-router"23export async function action({ request }: ActionFunctionArgs) {4const user = await requireUser({ request }) // 🔒 encore5const form = await request.formData()6// … mise à jour du profil …7return null8}
Sans ce garde-fou un utilisateur malveillant pourrait envoyer une requête
curl -X POST /profile et modifier n’importe quelle donnée.
Éviter la duplication côté client
La racine de l’app (app/root.tsx) charge toujours ;
profitons-en pour y remplir l’objet utilisateur une seule fois :
1export async function loader({ request }: LoaderFunctionArgs) {2const user = await getOptionalUser({ request })3return data({ user })4}
Puis expose-le via un hook maison :
1import { useRouteLoaderData } from "react-router"23export function useOptionalUser() {4const data = useRouteLoaderData<typeof loader>("root")5return data?.user ?? null6}
Maintenant n’importe quelle page fait simplement :
1const user = useOptionalUser()
sans requête supplémentaire.
Résumé du flux complet
1sequenceDiagram2participant B as Navigateur3participant SSR as Loader4participant DB as DB Prisma56B->>SSR: GET /profile (cookies?)7SSR->>SSR: requireUser()8SSR->>DB: SELECT user WHERE id = cookie9alt user trouvé10SSR-->>B: 200 + HTML11else pas de user12SSR-->>B: 302 Redirect /login13end
- Le cookie arrive dans l’entête
Cookie. requireUserlit le cookie, teste l’existence en BDD.- ✅ Présent : le rendu continue.
❌ Absent :
throw redirect("/login"), aucune autre ligne ne s’exécute.
Points clés
getOptionalUserlit l’id en cookie et vérifie qu’il existe en base.requireUserencapsule la redirection ; on l’appelle dans chaque loader et action à sécuriser.- Utilise
throw redirect()– pas besoin dereturn. - Charge l’utilisateur une seule fois dans
root.tsx; expose-le avecuseOptionalUser()pour éviter les requêtes redondantes. - La sécurité se joue côté serveur : même un fetch manuel doit passer par la garde.
Comprendre les concepts fondamentaux
Quelle est la principale différence entre les composants client et serveur dans React ?
Optimisation des performances
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Architecture des données
Quel hook permet de gérer les effets de bord dans un composant React ?