1. Authentification vs Autorisation
Ces deux concepts sont souvent confondus mais sont fondamentalement différents : l'authentification répond à la question "Qui êtes-vous ?" tandis que l'autorisation répond à "Qu'avez-vous le droit de faire ?".
Sessions vs JWT — Comparaison
| Critère | Sessions (cookies) | JWT (tokens) |
|---|---|---|
| Stockage côté serveur | Oui (mémoire / BDD) | Non (stateless) |
| Scalabilité | Complexe (sticky sessions) | Excellente |
| Révocation immédiate | Facile | Nécessite une blacklist |
| Mobile / API | Difficile (cookies) | Idéal (header Authorization) |
| Microservices | Problématique | Parfait |
| Taille | Petite (ID session) | Plus grand (payload) |
Flux typique avec JWT
// Flux complet d'une authentification JWT
//
// Client Serveur
// | |
// |-- POST /auth/register --> | 1. Inscription
// |<-- { token, user } ------ 2. Token créé et retourné
// | |
// |-- POST /auth/login -----> | 3. Connexion
// |<-- { token, user } ------ 4. Token valide
// | |
// |-- GET /api/profile -----> | 5. Requête protégée
// | Authorization: Bearer
// |<-- { user: { ... } } ---- 6. Réponse si token valide
// |<-- 401 Unauthorized ----- 7. Réponse si token invalide/expiré
2. JWT — JSON Web Token
Un JWT est un token signé en base64 composé de 3 parties séparées par des points :
header.payload.signature. Sa signature garantit l'intégrité —
toute modification invalide le token.
Structure d'un JWT
// Un JWT ressemble à ceci :
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20iLCJpYXQiOjE2MzQ1Njc4OTAsImV4cCI6MTYzNTE3MjY5MH0.abc123...
//
// Décodé :
// Header : { "alg": "HS256", "typ": "JWT" }
// Payload : { "userId": 1, "email": "alice@example.com", "iat": 1634567890, "exp": 1635172690 }
// Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)
//
// âš ï¸ Le payload est LISIBLE (base64), pas chiffré — ne pas y mettre de secrets !
Installation et import
# Installation
npm install jsonwebtoken
jwt.sign — Créer un token
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-prod';
// Créer un token avec expiration 7 jours
const token = jwt.sign(
{
userId: user.id,
email: user.email,
name: user.name,
role: user.role // données utiles dans chaque requête
},
JWT_SECRET,
{
expiresIn: '7d', // '15m', '1h', '7d', '30d', '1y'
issuer: 'mon-api', // optionnel — identifiant de l'émetteur
}
);
console.log('Token créé :', token.substring(0, 30) + '...');
jwt.verify — Vérifier et décoder
// jwt.verify lève une exception si le token est invalide ou expiré
try {
const decoded = jwt.verify(token, JWT_SECRET);
console.log('Payload décodé :', decoded);
// { userId: 1, email: 'alice@example.com', iat: 1634567890, exp: 1635172690 }
console.log('Expire le :', new Date(decoded.exp * 1000).toLocaleString());
} catch (err) {
if (err.name === 'TokenExpiredError') {
console.log('Token expiré le :', err.expiredAt);
} else if (err.name === 'JsonWebTokenError') {
console.log('Token invalide :', err.message);
}
}
// Décoder SANS vérification (utile pour debug uniquement)
const payload = jwt.decode(token);
console.log('Payload (non vérifié) :', payload);
3. bcryptjs — Hacher les mots de passe
Ne jamais stocker un mot de passe en clair dans une base de données.
Un attaquant qui accède à votre BDD ne doit pas pouvoir retrouver les mots de passe originaux.
bcrypt est un algorithme de hachage spécialement conçu pour les mots de passe :
il est intentionnellement lent et intègre un salt automatique.
Pourquoi bcrypt plutôt que MD5 / SHA256 ?
- Salt automatique : chaque hash est unique même pour le même mot de passe
- Cost factor : on peut augmenter la difficulté de calcul avec le temps
- Protection rainbow tables : le salt rend inutiles les tables pré-calculées
- MD5/SHA256 sont trop rapides — un GPU peut tester des milliards de combinaisons/seconde
Installation et usage
# bcryptjs est une implémentation pure JavaScript (pas de binaire natif)
npm install bcryptjs
bcrypt.hash — Hacher un mot de passe
const bcrypt = require('bcryptjs');
// Lors de l'inscription
async function hashPassword(password) {
// saltRounds = 10 → bon compromis sécurité/performance (prod)
// saltRounds = 12 → plus sécurisé mais 4x plus lent
// saltRounds = 6 → pour les tests uniquement
const hash = await bcrypt.hash(password, 10);
return hash;
// Ex: '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy'
}
// Exemple d'usage
const passwordBrut = 'MonMotDePasse123!';
const hash = await hashPassword(passwordBrut);
console.log('Hash généré :', hash);
// Le hash contient le salt intégré — c'est un format self-contained
bcrypt.compare — Vérifier un mot de passe
// Lors de la connexion — comparer le mot de passe saisi avec le hash stocké
async function verifierPassword(motDePasse, hashStocke) {
const valide = await bcrypt.compare(motDePasse, hashStocke);
return valide; // true si correct, false sinon
}
// Exemple
const motDePasseSaisi = 'MonMotDePasse123!';
const hashEnBase = '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy';
const ok = await verifierPassword(motDePasseSaisi, hashEnBase);
console.log(ok ? '✅ Mot de passe correct' : '⌠Mot de passe incorrect');
// âš ï¸ Ne JAMAIS comparer les chaînes directement : password === hash → FAUX !
4. Routes register et login
Les routes d'authentification sont les plus sensibles de votre API. Elles doivent valider les entrées, gérer les erreurs précisément et ne jamais retourner le mot de passe hashé dans la réponse.
POST /auth/register — Inscription
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const router = express.Router();
// Stockage en mémoire pour l'exemple (utiliser une BDD en prod)
let users = [];
// POST /auth/register
router.post('/register', async (req, res) => {
try {
const { name, email, password } = req.body;
// 1. Validation des champs
if (!name || !email || !password) {
return res.status(400).json({ success: false, error: 'name, email et password requis' });
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return res.status(422).json({ success: false, error: 'Format email invalide' });
}
if (password.length < 6) {
return res.status(422).json({ success: false, error: 'Mot de passe: 6 caractères minimum' });
}
// 2. Vérifier si l'email est déjà utilisé
if (users.find(u => u.email === email)) {
return res.status(409).json({ success: false, error: 'Email déjà utilisé' });
}
// 3. Hacher le mot de passe
const passwordHash = await bcrypt.hash(password, 10);
// 4. Créer l'utilisateur (SANS retourner passwordHash)
const user = { id: Date.now().toString(), name, email, passwordHash, createdAt: new Date().toISOString() };
users.push(user);
// 5. Créer et retourner le token
const token = jwt.sign(
{ userId: user.id, email: user.email, name: user.name },
process.env.JWT_SECRET || 'dev-secret',
{ expiresIn: '7d' }
);
res.status(201).json({
success: true,
token,
user: { id: user.id, name: user.name, email: user.email } // pas de passwordHash !
});
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
});
POST /auth/login — Connexion
// POST /auth/login
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ success: false, error: 'email et password requis' });
}
// 1. Trouver l'utilisateur par email
const user = users.find(u => u.email === email);
// 2. Message d'erreur IDENTIQUE que l'email n'existe pas ou que le mot de passe soit faux
// (protection contre l'énumération d'utilisateurs)
if (!user) {
return res.status(401).json({ success: false, error: 'Email ou mot de passe incorrect' });
}
// 3. Vérifier le mot de passe contre le hash
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) {
return res.status(401).json({ success: false, error: 'Email ou mot de passe incorrect' });
}
// 4. Créer et retourner le token
const token = jwt.sign(
{ userId: user.id, email: user.email, name: user.name },
process.env.JWT_SECRET || 'dev-secret',
{ expiresIn: '7d' }
);
res.json({
success: true,
token,
user: { id: user.id, name: user.name, email: user.email }
});
} catch (err) {
res.status(500).json({ success: false, error: err.message });
}
});
module.exports = router;
5. Middleware verifyToken et routes protégées
Le middleware verifyToken est le gardien de vos routes protégées.
Il extrait le token du header Authorization: Bearer <token>,
le vérifie cryptographiquement et attache l'utilisateur décodé à req.user.
Middleware verifyToken complet
const jwt = require('jsonwebtoken');
function verifyToken(req, res, next) {
// 1. Lire le header Authorization
const authHeader = req.headers.authorization;
// 2. Vérifier le format "Bearer "
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
error: 'Token manquant. Utilisez: Authorization: Bearer '
});
}
// 3. Extraire le token (supprimer "Bearer ")
const token = authHeader.slice(7); // ou .split(' ')[1]
try {
// 4. Vérifier et décoder le token
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'dev-secret');
// 5. Attacher l'utilisateur à la requête
req.user = decoded;
// req.user = { userId: '123', email: '...', name: '...', iat: ..., exp: ... }
next(); // Passer au prochain handler
} catch (err) {
// 6. Gérer les erreurs spécifiques JWT
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
error: 'Token expiré — veuillez vous reconnecter'
});
}
// JsonWebTokenError, NotBeforeError, etc.
return res.status(401).json({
success: false,
error: 'Token invalide'
});
}
}
module.exports = verifyToken;
Utiliser verifyToken sur les routes protégées
const express = require('express');
const verifyToken = require('./middlewares/verifyToken');
const app = express();
app.use(express.json());
// Route publique — pas de middleware
app.post('/auth/login', loginHandler);
app.post('/auth/register', registerHandler);
// Route protégée — verifyToken en 2ème argument
app.get('/auth/profile', verifyToken, async (req, res) => {
// req.user est disponible ici grâce au middleware
const { userId, email, name } = req.user;
res.json({
success: true,
user: { userId, email, name }
});
});
// Autre route protégée — l'utilisateur ne peut modifier QUE ses propres données
app.put('/users/:id', verifyToken, async (req, res) => {
if (req.params.id !== req.user.userId) {
return res.status(403).json({ success: false, error: 'Accès interdit' });
}
// ... mise à jour
});
// Middleware sur un groupe de routes entier
const adminRouter = express.Router();
adminRouter.use(verifyToken); // Protège toutes les routes du router
adminRouter.get('/stats', (req, res) => res.json({ stats: '...' }));
openssl rand -hex 64). Ne la commitez jamais dans git —
stockez-la dans une variable d'environnement.