Chapitre 23 — Authentification : login/logout, routes protégées, stockage du token
L’authentification est un passage obligé dans la plupart des applications : espace membre, back‑office, panier, profil utilisateur, etc. Dans ce chapitre, on met en place une base simple et propre : un formulaire de login, un token stocké (version débutant), un état “connecté / déconnecté” centralisé, et des routes protégées avec React Router.
Plan du chapitre
- 1) Notions : authentification vs autorisation
- 2) Le scénario qu’on va implémenter
- 3) API : endpoint /login (mock ou backend)
- 4) Login : formulaire contrôlé + validations
- 5) Stocker le token : version débutant (localStorage)
- 6) Centraliser l’état : AuthContext + useAuth()
- 7) Brancher un vrai backend : services/ + auth.service.js
- 8) Recharger l’utilisateur au démarrage : endpoint /me
- 9) Gestion des erreurs 401/403 : quoi faire côté React ?
- 10) Routes protégées (React Router)
- 11) Logout (déconnexion) + nettoyage
- 12) Sécurité : bonnes pratiques (sans paniquer)
- 13) Résumé
- 14) Exercice pratique
1) Notions : authentification vs autorisation
Authentification : “Qui es‑tu ?” (ex : tu te connectes avec un email + mot de passe).
Autorisation : “As‑tu le droit ?” (ex : accès admin, accès à une page privée, etc.).
Dans ce chapitre, on met en place l’authentification côté React, et une autorisation simple : certaines routes seront accessibles uniquement si l’utilisateur est connecté.
2) Le scénario qu’on va implémenter
On veut :
- Une page Login avec email + mot de passe
- Un token reçu en réponse (simulé ou venant d’un backend)
- Un état global :
user/token/isAuthenticated - Une page Profil accessible seulement si connecté
- Un bouton Logout
React est le client. Le backend (ou le mock) est responsable de vérifier les identifiants et de renvoyer un token.
3) API : endpoint /login (mock ou backend)
Dans la vraie vie, votre backend expose souvent un endpoint de type :
POST /login qui renvoie un JSON.
Exemple de réponse attendue
{
"token": "xxxxx.yyyyy.zzzzz",
"user": {
"id": 1,
"email": "gandalf@mail.com",
"name": "Gandalf"
}
}
Pour un tutoriel, vous pouvez aussi “mock” l’API (simuler une réponse) le temps d’apprendre la mécanique. L’important ici est la structure : appel → réponse → stockage → navigation.
4) Login : formulaire contrôlé + validations
On crée un composant LoginPage avec un formulaire.
“Contrôlé” signifie : les valeurs des inputs sont stockées dans le state.
import { useState } from "react";
import { useAuth } from "../contexts/AuthContext.jsx";
export default function LoginPage() {
const { login } = useAuth();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
// validations très simples (débutant)
if (!email.trim() || !password.trim()) {
setError("Email et mot de passe sont requis.");
return;
}
setError(null);
setLoading(true);
try {
await login({ email, password });
} catch (err) {
setError(err.message || "Connexion impossible.");
} finally {
setLoading(false);
}
}
return (
<div>
<h2>Connexion</h2>
{error && <p>Erreur : {error}</p>}
<form onSubmit={handleSubmit}>
<label>
Email
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</label>
<label>
Mot de passe
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</label>
<button type="submit" disabled={loading}>
{loading ? "Connexion..." : "Se connecter"}
</button>
</form>
</div>
);
}
Le composant appelle login() fourni par un contexte d’authentification.
C’est lui qui fera l’appel API et stockera le token.
5) Stocker le token : version débutant (localStorage)
Pour débuter, on stocke le token dans localStorage afin de rester connecté après un refresh.
Attention : c’est pratique, mais pas parfait niveau sécurité (on en parle plus loin).
localStorage.setItem("token", token);
const saved = localStorage.getItem("token");
localStorage.removeItem("token");
6) Centraliser l’état : AuthContext + useAuth()
On crée un contexte pour éviter de passer le token en props partout.
Le contexte fournit : user, token, isAuthenticated, login(), logout().
Fichier : src/contexts/AuthContext.jsx
import { createContext, useContext, useMemo, useState } from "react";
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [token, setToken] = useState(() => localStorage.getItem("token") || "");
const [user, setUser] = useState(() => {
const raw = localStorage.getItem("user");
return raw ? JSON.parse(raw) : null;
});
const isAuthenticated = !!token;
async function login({ email, password }) {
// Version tutoriel (mock) : remplacez par un vrai appel API ensuite.
if (email === "gandalf@mail.com" && password === "12345") {
const fakeToken = "demo-token-123";
const fakeUser = { id: 1, email, name: "Gandalf" };
setToken(fakeToken);
setUser(fakeUser);
localStorage.setItem("token", fakeToken);
localStorage.setItem("user", JSON.stringify(fakeUser));
return;
}
throw new Error("Identifiants incorrects.");
}
function logout() {
setToken("");
setUser(null);
localStorage.removeItem("token");
localStorage.removeItem("user");
}
const value = useMemo(() => ({
token,
user,
isAuthenticated,
login,
logout
}), [token, user, isAuthenticated]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth doit être utilisé dans un AuthProvider.");
return ctx;
}
Le mock sert à pratiquer le “flow”. Ensuite, remplacez login() par un appel via services/
(chapitre 22) vers un vrai backend.
7) Brancher un vrai backend : services/ + auth.service.js
Jusqu’ici, on a utilisé un mock pour apprendre la mécanique. Maintenant, on fait la version “projet”
en s’appuyant sur la structure vue au chapitre 22 : un dossier services/ qui contient
des fonctions d’accès API réutilisables.
Structure recommandée
src/
services/
apiClient.js
auth.service.js
contexts/
AuthContext.jsx
routes/
ProtectedRoute.jsx
1) Un client API réutilisable : services/apiClient.js
Objectif : centraliser fetch, gérer les erreurs et ajouter automatiquement le token si présent.
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:3000";
function getToken() {
return localStorage.getItem("token") || "";
}
async function request(path, { method = "GET", body } = {}) {
const headers = { "Content-Type": "application/json" };
const token = getToken();
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(`${API_BASE_URL}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
// Gestion d’erreur standardisée
if (!res.ok) {
let message = "Erreur réseau.";
try {
const data = await res.json();
message = data?.message || message;
} catch {}
const err = new Error(message);
err.status = res.status;
throw err;
}
// Certaines routes peuvent renvoyer 204 No Content
if (res.status === 204) return null;
return res.json();
}
export const apiClient = {
get: (path) => request(path),
post: (path, body) => request(path, { method: "POST", body }),
};
2) Le service d’auth : services/auth.service.js
import { apiClient } from "./apiClient.js";
export async function loginService({ email, password }) {
// Backend attendu : POST /login
return apiClient.post("/login", { email, password });
}
export async function meService() {
// Backend attendu : GET /me (retourne l’utilisateur courant)
return apiClient.get("/me");
}
Si votre backend n’utilise pas /login et /me, adaptez simplement les routes. Le principe
(service dédié + client API) reste identique.
8) Recharger l’utilisateur au démarrage : endpoint /me
Problème classique : vous rechargez la page, le token est dans localStorage, mais vous n’avez plus
l’objet user en mémoire (state). Solution : au démarrage, si un token existe, appeler /me.
Mise à jour de AuthContext (version backend)
import { createContext, useContext, useEffect, useMemo, useState } from "react";
import { loginService, meService } from "../services/auth.service.js";
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [token, setToken] = useState(() => localStorage.getItem("token") || "");
const [user, setUser] = useState(null);
const [loadingAuth, setLoadingAuth] = useState(true);
const isAuthenticated = !!token;
async function login({ email, password }) {
const { token: newToken, user: newUser } = await loginService({ email, password });
setToken(newToken);
setUser(newUser);
localStorage.setItem("token", newToken);
}
function logout() {
setToken("");
setUser(null);
localStorage.removeItem("token");
}
useEffect(() => {
async function bootstrap() {
// Si pas de token, rien à recharger
if (!token) {
setLoadingAuth(false);
return;
}
try {
const me = await meService();
setUser(me);
} catch (err) {
// Si token invalide/expiré, on force logout
if (err.status === 401) logout();
} finally {
setLoadingAuth(false);
}
}
bootstrap();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const value = useMemo(() => ({
token,
user,
isAuthenticated,
loadingAuth,
login,
logout,
}), [token, user, isAuthenticated, loadingAuth]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth doit être utilisé dans un AuthProvider.");
return ctx;
}
Note : si vous souhaitez que la requête /me se relance quand token change, vous pouvez
mettre [token] en dépendance, mais pour un flux débutant, le bootstrap “au montage” suffit.
9) Gestion des erreurs 401/403 : quoi faire côté React ?
- 401 Unauthorized : l’utilisateur n’est pas authentifié (token manquant/expiré). → rediriger vers login.
- 403 Forbidden : l’utilisateur est authentifié mais n’a pas les droits (rôle). → afficher “accès refusé”.
Réflexe simple
Si une requête renvoie 401, votre app doit considérer que la session n’est plus valide :
logout + redirection vers /login.
try {
const data = await apiClient.get("/private-data");
// ...
} catch (err) {
if (err.status === 401) {
// logout + redirection (au choix)
}
}
10) Routes protégées (React Router)
Une route protégée redirige vers /login si l’utilisateur n’est pas connecté.
Pour ça, on crée un composant ProtectedRoute.
Fichier : src/routes/ProtectedRoute.jsx
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext.jsx";
export default function ProtectedRoute() {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
}
Exemple de routes
import { Routes, Route } from "react-router-dom";
import LoginPage from "./pages/LoginPage.jsx";
import ProfilePage from "./pages/ProfilePage.jsx";
import ProtectedRoute from "./routes/ProtectedRoute.jsx";
export default function AppRoutes() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route element={<ProtectedRoute />}>
<Route path="/profile" element={<ProfilePage />} />
</Route>
</Routes>
);
}
11) Logout (déconnexion) + nettoyage
import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext.jsx";
export default function LogoutButton() {
const { logout } = useAuth();
const navigate = useNavigate();
function handleLogout() {
logout();
navigate("/login");
}
return <button onClick={handleLogout}>Se déconnecter</button>;
}
12) Sécurité : bonnes pratiques (sans paniquer)
Débutant : localStorage est pratique, mais un XSS peut potentiellement le lire.
En production, beaucoup d’apps préfèrent des cookies HTTPOnly (côté serveur).
- Apprendre la mécanique d’abord, optimiser la sécurité ensuite.
- Ne jamais injecter du HTML non fiable (risque XSS).
- Ne stockez pas d’informations sensibles “en clair” côté client.
13) Résumé
- Login : formulaire contrôlé → appel
login() - Stockage token (débutant) :
localStorage - Auth global :
AuthContext+useAuth() - Routes privées :
ProtectedRoute - Logout : nettoyage token + user
14) Exercice pratique
- Créer une page
DashboardPageprotégée (/dashboard). - Sur le dashboard, afficher
user.namesi connecté. - Créer une
NavBar: afficher “Login” si déconnecté, “Logout” si connecté. - Bonus : remplacer le mock par un vrai
loginService()viaservices/auth.service.js(chapitre 22). - Bonus 2 : ajouter un appel
/meau démarrage et gérer le cas401en forçant un logout.
Conseil : commencez par le mock, puis branchez le backend une fois le flow maîtrisé.