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é.

5 min read
Déverrouillez votre potentiel

avec React Router 7

Vous en avez marre de...

❌ perdre du temps à chercher des informations éparpillées
❌ ne pas avoir de retour sur votre progression
Assistant IA spécialisé

Posez vos questions 24/7 à notre IA experte en React Router 7

Quiz interactifs

Validez vos acquis avec des quiz personnalisés et un feedback instantané

9 modules
45 leçons
Accès à vie
299.00
-50%

Objectif de la leçon

Nous allons passer d’une « fausse » authentification (ID codé en dur dans le cookie) à une authentification réelle :

  1. ajout d’un champ password hashé dans la base Prisma ;
  2. hachage/seed initial avec bcryptjs ;
  3. formulaire de connexion (slug + mot de passe) validé par Zod + Conform ;
  4. 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

prisma/schema.prisma
1
model User {
2
id Int @id @default(autoincrement())
3
firstName String
4
lastName String
5
slug String @unique
6
age Int
7
active Boolean @default(true)
8
password String // ← nouveau champ NON optionnel
9
}

⚠️ La table contient déjà des lignes. Prisma refuse d’ajouter une colonne obligatoire si elle n’a pas de valeur.

Deux stratégies

  1. rendre password optionnel le temps d’une migration, peu élégant ;
  2. réinitialiser la base (choix le plus simple au stade du cours).
Terminal
1
npx prisma migrate reset # Purge + recrée les tables
2
npx prisma migrate dev --name add_password_field

2. Séeder des mots de passe hashés

Installe le hash :

Terminal
1
npm install bcryptjs

Crée un helper réutilisable :

app/server/sessions.server.ts
1
import { hash , compare } from "bcryptjs";
2
3
export const hashPassword = async ({ password }: { password: string }) =>
4
await hash(password, 10);
5
6
export const comparePasswords = ({
7
password,
8
hashedPassword,
9
}: {
10
password: string;
11
hashedPassword: string;
12
}) => await compare(password, hashedPassword);

Utilise-le dans le seed :

prisma/seed.ts
1
import { prisma } from "~/server/db.server";
2
import { hashPassword } from "~/server/sessions.server";
3
4
await prisma.user.deleteMany();
5
6
await prisma.user.create({
7
data: {
8
firstName: "Virgile",
9
lastName: "Rietsch",
10
slug: "virgile",
11
age: 28,
12
active: true,
13
password: await hashPassword({ password: "abc123" }),
14
},
15
});
16
/* … autres utilisateurs … */
Terminal
1
npx 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 :

app/server/sessions.server.ts
1
export async function checkIfUserExists({
2
slug,
3
password,
4
}: {
5
slug: string;
6
password: string;
7
}) {
8
const user = await prisma.user.findUnique({
9
where: { slug },
10
select: { id: true, password: true },
11
});
12
13
if (!user) return { userExists: false, isPasswordValid: false, userId: null };
14
15
const isPasswordValid = await comparePasswords({
16
password,
17
hashedPassword: user.password,
18
});
19
20
return { 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é

app/routes/login.tsx
1
const LoginSchema = z.object({
2
slug: z.string({ required_error: "Le slug est obligatoire" }),
3
password: z.string({ required_error: "Le mot de passe est obligatoire" }),
4
});
app/routes/login.tsx
1
export default function Login() {
2
const actionData = useActionData<typeof action>();
3
const [form, fields] = useForm({
4
lastResult: actionData?.result,
5
constraint: getZodConstraint(LoginSchema),
6
onValidate: ({ formData }) =>
7
parseWithZod(formData, { schema: LoginSchema }),
8
});
9
10
return (
11
<Form method="POST" {...getFormProps(form)} className="max-w-[300px] mx-auto">
12
<Field labelProps={{ children: "Slug" }}
13
inputProps={getInputProps(fields.slug, { type: "text" })}
14
errors={fields.slug.errors} />
15
<Field labelProps={{ children: "Mot de passe" }}
16
inputProps={getInputProps(fields.password, { type: "password" })}
17
errors={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

app/routes/login.tsx
1
export async function action({ request }: ActionFunctionArgs) {
2
const formData = await request.formData();
3
4
const submission = await parseWithZod(formData, {
5
async: true,
6
schema: LoginSchema.superRefine(async (data, ctx) => {
7
const { userExists, isPasswordValid } = await checkIfUserExists(data);
8
if (!userExists || !isPasswordValid) {
9
ctx.addIssue({
10
path: ["slug"], // on évite de leak l’info
11
code: "custom",
12
message: "Les identifiants sont invalides",
13
});
14
}
15
}),
16
});
17
18
if (submission.status !== "success")
19
return data({ result: submission.reply() }, { status: 400 });
20
21
const { userId } = await checkIfUserExists(submission.value); // garanti OK
22
const session = await getUserSession({ request });
23
session.set("userId", String(userId));
24
25
const redirectTo =
26
new URL(request.url).searchParams.get("redirectTo") || "/";
27
28
return redirect(redirectTo, {
29
headers: { "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

app/routes/logout.tsx
1
export async function action({ request }: ActionFunctionArgs) {
2
const url = new URL(request.url);
3
const redirectTo = url.searchParams.get("redirectTo") || "/";
4
return await logout({ request, redirectTo });
5
}

Et le bouton :

app/root.tsx
1
const { pathname } = useLocation();
2
3
<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 password non optionnel ⇒ migration + reset de la DB.
  • Hash bcrypt côté seed et côté inscription pour ne jamais stocker de clair.
  • Helper checkIfUserExists centralise 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 userId dans le cookie sécurisé avec commitSession, puis redirection vers redirectTo.
  • Déconnexion = simple route POST + logout() (cookie supprimé + retour à l’URL courante)
Premium
Quiz interactif
Testez vos connaissances et validez votre compréhension du module avec notre quiz interactif.
1

Comprendre les concepts fondamentaux

Quelle est la principale différence entre les composants client et serveur dans React ?

Les composants client s'exécutent uniquement dans le navigateur
Les composants serveur peuvent utiliser useState
Les composants client sont plus rapides
Il n'y a aucune différence significative
2

Optimisation des performances

Quelle technique est recommandée pour éviter les rendus inutiles dans React ?

Utiliser React.memo pour les composants fonctionnels
Ajouter plus d'états locaux
Éviter d'utiliser les props
Toujours utiliser les class components
3

Architecture des données

Quel hook permet de gérer les effets de bord dans un composant React ?

useEffect
useState
useMemo
useContext
4

Gestion des erreurs

Comment implémenter la gestion des erreurs pour les requêtes API dans React ?

Utiliser try/catch avec async/await
Ignorer les erreurs
Toujours afficher un message d'erreur
Rediriger l'utilisateur
5

Déploiement et CI/CD

Quelle est la meilleure pratique pour déployer une application React en production ?

Utiliser un service CI/CD comme GitHub Actions
Copier les fichiers manuellement via FTP
Envoyer le code source complet
Ne jamais mettre à jour l'application

Débloquez ce quiz et tous les autres contenus premium en achetant ce cours