Apprends à sécuriser l'authentification dans React Router 7 avec Prisma, bcryptjs et validation Zod pour un login fiable et typé.
avec React Router 7
Posez vos questions 24/7 à notre IA experte en React Router 7
Validez vos acquis avec des quiz personnalisés et un feedback instantané
Nous allons passer d’une « fausse » authentification (ID codé en dur dans le cookie) à une authentification réelle :
password
hashé dans la base Prisma ;bcryptjs
;Résultat : un workflow sécurisé, sans divulgation d’informations et totalement typé.
password
dans Prisma1model User {2id Int @id @default(autoincrement())3firstName String4lastName String5slug String @unique6age Int7active Boolean @default(true)8password String // ← nouveau champ NON optionnel9}
⚠️ La table contient déjà des lignes. Prisma refuse d’ajouter une colonne obligatoire si elle n’a pas de valeur.
password
optionnel le temps d’une migration, peu élégant ;1npx prisma migrate reset # Purge + recrée les tables2npx prisma migrate dev --name add_password_field
Installe le hash :
1npm install bcryptjs
Crée un helper réutilisable :
1import { hash , compare } from "bcryptjs";23export const hashPassword = async ({ password }: { password: string }) =>4await hash(password, 10);56export const comparePasswords = ({7password,8hashedPassword,9}: {10password: string;11hashedPassword: string;12}) => await compare(password, hashedPassword);
Utilise-le dans le seed :
1import { prisma } from "~/server/db.server";2import { hashPassword } from "~/server/sessions.server";34await prisma.user.deleteMany();56await prisma.user.create({7data: {8firstName: "Virgile",9lastName: "Rietsch",10slug: "virgile",11age: 28,12active: true,13password: await hashPassword({ password: "abc123" }),14},15});16/* … autres utilisateurs … */
1npx prisma db seed
Dans Prisma Studio tu vois maintenant une longue chaîne hashée : le mot de passe n’est plus en clair.
Crée un service utilitaire :
1export async function checkIfUserExists({2slug,3password,4}: {5slug: string;6password: string;7}) {8const user = await prisma.user.findUnique({9where: { slug },10select: { id: true, password: true },11});1213if (!user) return { userExists: false, isPasswordValid: false, userId: null };1415const isPasswordValid = await comparePasswords({16password,17hashedPassword: user.password,18});1920return { userExists: true, isPasswordValid, userId: user.id };21}
Ce helper renvoie :
userExists
– le slug est-il connu ?isPasswordValid
– le hash correspond-il ?userId
– pour la session si tout va bien.1const LoginSchema = z.object({2slug: z.string({ required_error: "Le slug est obligatoire" }),3password: z.string({ required_error: "Le mot de passe est obligatoire" }),4});
1export default function Login() {2const actionData = useActionData<typeof action>();3const [form, fields] = useForm({4lastResult: actionData?.result,5constraint: getZodConstraint(LoginSchema),6onValidate: ({ formData }) =>7parseWithZod(formData, { schema: LoginSchema }),8});910return (11<Form method="POST" {...getFormProps(form)} className="max-w-[300px] mx-auto">12<Field labelProps={{ children: "Slug" }}13inputProps={getInputProps(fields.slug, { type: "text" })}14errors={fields.slug.errors} />15<Field labelProps={{ children: "Mot de passe" }}16inputProps={getInputProps(fields.password, { type: "password" })}17errors={fields.password.errors} />18<button className="w-full p-2 rounded-md bg-sky-600 text-white">Login</button>19</Form>20);21}
1export async function action({ request }: ActionFunctionArgs) {2const formData = await request.formData();34const submission = await parseWithZod(formData, {5async: true,6schema: LoginSchema.superRefine(async (data, ctx) => {7const { userExists, isPasswordValid } = await checkIfUserExists(data);8if (!userExists || !isPasswordValid) {9ctx.addIssue({10path: ["slug"], // on évite de leak l’info11code: "custom",12message: "Les identifiants sont invalides",13});14}15}),16});1718if (submission.status !== "success")19return data({ result: submission.reply() }, { status: 400 });2021const { userId } = await checkIfUserExists(submission.value); // garanti OK22const session = await getUserSession({ request });23session.set("userId", String(userId));2425const redirectTo =26new URL(request.url).searchParams.get("redirectTo") || "/";2728return redirect(redirectTo, {29headers: { "Set-Cookie": await commitSession(session) },30});31}
Double validation :
– Zod côté client bloque les champs vides ;
– Zod + checkIfUserExists
côté serveur refuse slug inconnu ou mauvais pass.
Message unique → pas de « slug inexistant » qui donnerait un indice à un attaquant (protection credential enumeration).
1export async function action({ request }: ActionFunctionArgs) {2const url = new URL(request.url);3const redirectTo = url.searchParams.get("redirectTo") || "/";4return await logout({ request, redirectTo });5}
Et le bouton :
1const { pathname } = useLocation();23<Form method="POST" action={`${href("/logout")}?redirectTo=${pathname}`}>4<button>Logout</button>5</Form>
Le helper logout()
détruit le cookie via destroySession
puis redirige.
Tu peux donc changer de compte sans détour par la home.
password
non optionnel ⇒ migration + reset de la DB.checkIfUserExists
centralise vérification slug + mot de passe.userId
dans le cookie sécurisé avec
commitSession
, puis redirection vers redirectTo
.logout()
(cookie supprimé + retour à l’URL courante)Quelle est la principale différence entre les composants client et serveur dans React ?
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Quel hook permet de gérer les effets de bord dans un composant React ?
Comment implémenter la gestion des erreurs pour les requêtes API dans React ?
Quelle est la meilleure pratique pour déployer une application React en production ?