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èreSessions (cookies)JWT (tokens)
Stockage côté serveurOui (mémoire / BDD)Non (stateless)
ScalabilitéComplexe (sticky sessions)Excellente
Révocation immédiateFacileNécessite une blacklist
Mobile / APIDifficile (cookies)Idéal (header Authorization)
MicroservicesProblématiqueParfait
TaillePetite (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é
💡 Principe de moindre privilège : n'accordez que les permissions strictement nécessaires. Un utilisateur standard ne doit pas avoir accès aux routes admin.

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);
⚠️ Jamais de jwt.decode() en production pour l'authentification ! Utilisez toujours jwt.verify() qui valide la signature cryptographique.

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 !
💡 En cas d'email ou mot de passe incorrect, retournez toujours le même message d'erreur ("Email ou mot de passe incorrect") pour ne pas révéler si l'email existe ou non. C'est une protection contre l'énumération.

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: '...' }));
⚠️ JWT_SECRET en production : utilisez une chaîne longue et aléatoire (ex: openssl rand -hex 64). Ne la commitez jamais dans git — stockez-la dans une variable d'environnement.
← N09 Base de données Exercices →