Comment intégrer l'authentification par mot de passe
Apprends à sécuriser l'authentification dans React Router 7 avec Prisma, bcryptjs et validation Zod pour un login fiable et typé.
Objectif de la leçon
Nous allons passer d’une « fausse » authentification (ID codé en dur dans le cookie) à une authentification réelle :
- ajout d’un champ
passwordhashé dans la base Prisma ; - hachage/seed initial avec
bcryptjs; - formulaire de connexion (slug + mot de passe) validé par Zod + Conform ;
- vérification serveur puis création de la session.
Résultat : un workflow sécurisé, sans divulgation d’informations et totalement typé.
1. Ajouter le champ password dans Prisma
1model 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.
Deux stratégies
- rendre
passwordoptionnel le temps d’une migration, peu élégant ; - réinitialiser la base (choix le plus simple au stade du cours).
1npx prisma migrate reset # Purge + recrée les tables2npx prisma migrate dev --name add_password_field
2. Séeder des mots de passe hashés
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.
3. Vérifier login et mot de passe côté serveur
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.
4. Formulaire de connexion typé
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}
5. Action : validation + session
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).
6. Sécuriser la déconnexion
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.
7. Points clés
- Champ
passwordnon optionnel ⇒ migration + reset de la DB. - Hash bcrypt côté seed et côté inscription pour ne jamais stocker de clair.
- Helper
checkIfUserExistscentralise vérification slug + mot de passe. - Validation Conform + Zod : feedback en temps réel, erreurs serveur fusionnées.
- Un message d’erreur générique évite de révéler si le slug existe.
- Après succès : on écrit
userIddans le cookie sécurisé aveccommitSession, puis redirection versredirectTo. - Déconnexion = simple route POST +
logout()(cookie supprimé + retour à l’URL courante)
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 ?