Node.js

Authentification JWT avec Node.js : guide complet

Mark Toledo

Mark Toledo

1 avril 2026

Authentification JWT avec Node.js : guide complet

J'ai implémenté des systèmes d'authentification de dizaines de façons différentes au fil des années. Sessions avec Redis, OAuth, API keys, et bien sûr JWT. Chaque approche a ses contextes d'usage, mais JWT est devenu incontournable pour les APIs stateless — particulièrement quand tu travailles avec des microservices ou des clients mobiles.

Ce guide part du principe que tu as déjà un projet Node.js avec Express. On va construire une authentification JWT complète, avec les pièges à éviter que j'ai appris à mes dépens.

Ce qu'est vraiment un JWT

Un JSON Web Token est une chaîne encodée en base64 composée de trois parties séparées par des points : header, payload, signature.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImVtYWlsIjoibWFya0BleGFtcGxlLmNvbSIsImlhdCI6MTcwNDAwMDAwMCwiZXhwIjoxNzA0MDg2NDAwfQ.abc123signature

Le payload est lisible par n'importe qui (il est juste encodé, pas chiffré). La signature, elle, permet de vérifier que le token n'a pas été modifié. C'est un point fondamental : ne jamais stocker d'informations sensibles dans le payload JWT.

Installation et configuration

npm install jsonwebtoken bcrypt express

On commence par définir nos variables d'environnement. Ne jamais hardcoder le secret JWT dans le code :

// config.js
module.exports = {
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '24h',
    refreshSecret: process.env.JWT_REFRESH_SECRET,
    refreshExpiresIn: '7d'
  }
};

Génération des tokens

// auth/tokenService.js
const jwt = require('jsonwebtoken');
const config = require('../config');

function genererToken(payload) {
  return jwt.sign(payload, config.jwt.secret, {
    expiresIn: config.jwt.expiresIn,
    issuer: 'mon-api.fr',
    audience: 'mon-app'
  });
}

function genererRefreshToken(userId) {
  return jwt.sign(
    { userId, type: 'refresh' },
    config.jwt.refreshSecret,
    { expiresIn: config.jwt.refreshExpiresIn }
  );
}

function verifierToken(token) {
  return jwt.verify(token, config.jwt.secret, {
    issuer: 'mon-api.fr',
    audience: 'mon-app'
  });
}

module.exports = { genererToken, genererRefreshToken, verifierToken };

La route de connexion

// routes/auth.js
const bcrypt = require('bcrypt');
const { genererToken, genererRefreshToken } = require('../auth/tokenService');

router.post('/connexion', async (req, res) => {
  const { email, motDePasse } = req.body;

  try {
    const utilisateur = await UtilisateurRepository.trouverParEmail(email);
    
    if (!utilisateur) {
      // Même message d'erreur que si le mdp est faux — évite l'énumération
      return res.status(401).json({ message: 'Identifiants incorrects' });
    }

    const mdpValide = await bcrypt.compare(motDePasse, utilisateur.motDePasseHash);
    if (!mdpValide) {
      return res.status(401).json({ message: 'Identifiants incorrects' });
    }

    const payload = { userId: utilisateur.id, email: utilisateur.email };
    const token = genererToken(payload);
    const refreshToken = genererRefreshToken(utilisateur.id);

    // Stocker le refresh token en DB pour pouvoir le révoquer
    await RefreshTokenRepository.creer(utilisateur.id, refreshToken);

    res.json({ token, refreshToken });
  } catch (erreur) {
    console.error('Erreur connexion:', erreur);
    res.status(500).json({ message: 'Erreur serveur' });
  }
});

Le middleware d'authentification

C'est la partie que je vois le plus souvent mal implémentée. Le middleware doit être propre, sans effets de bord, et retourner des erreurs claires.

// middleware/authentifier.js
const { verifierToken } = require('../auth/tokenService');

function authentifier(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ message: 'Token manquant' });
  }

  const token = authHeader.slice(7);

  try {
    const decoded = verifierToken(token);
    req.utilisateur = decoded;
    next();
  } catch (erreur) {
    if (erreur.name === 'TokenExpiredError') {
      return res.status(401).json({ 
        message: 'Token expiré',
        code: 'TOKEN_EXPIRED'
      });
    }
    
    return res.status(401).json({ message: 'Token invalide' });
  }
}

module.exports = { authentifier };

Le code TOKEN_EXPIRED dans la réponse permet au client de savoir qu'il doit tenter un refresh plutôt qu'une reconnexion complète.

Les refresh tokens : gérer l'expiration sans déconnecter l'utilisateur

Sur un projet client, des utilisateurs se plaignaient d'être déconnectés toutes les heures. On avait configuré une expiration courte pour des raisons de sécurité, mais sans refresh tokens pour compenser. La solution : des access tokens courts (15-60 minutes) et des refresh tokens longs (7-30 jours).

router.post('/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(400).json({ message: 'Refresh token requis' });
  }

  try {
    const decoded = jwt.verify(refreshToken, config.jwt.refreshSecret);
    
    // Vérifier que le token existe toujours en base (pas révoqué)
    const tokenEnDB = await RefreshTokenRepository.trouver(refreshToken);
    if (!tokenEnDB) {
      return res.status(401).json({ message: 'Refresh token révoqué' });
    }

    const utilisateur = await UtilisateurRepository.trouverParId(decoded.userId);
    const nouveauToken = genererToken({
      userId: utilisateur.id,
      email: utilisateur.email
    });

    res.json({ token: nouveauToken });
  } catch (erreur) {
    res.status(401).json({ message: 'Refresh token invalide' });
  }
});

Révocation des tokens

C'est le talon d'Achille de JWT : par nature, un token est valide jusqu'à son expiration. Pour révoquer un access token, tu as deux options.

La première : expiration courte. Un token de 15 minutes est "révoqué" dans un quart d'heure au pire.

La deuxième : blacklist en cache. À la déconnexion, tu ajoutes le jti (JWT ID) dans Redis avec une TTL égale à l'expiration restante du token.

router.post('/deconnexion', authentifier, async (req, res) => {
  // Révoquer le refresh token
  const { refreshToken } = req.body;
  if (refreshToken) {
    await RefreshTokenRepository.supprimer(refreshToken);
  }

  // Blacklister l'access token courant
  const token = req.headers.authorization.slice(7);
  const decoded = jwt.decode(token);
  const ttl = decoded.exp - Math.floor(Date.now() / 1000);
  
  if (ttl > 0) {
    await redis.setex(`blacklist:${decoded.jti}`, ttl, '1');
  }

  res.json({ message: 'Déconnexion réussie' });
});

Autorisation par rôles

L'authentification dit "qui tu es", l'autorisation dit "ce que tu peux faire". Un middleware simple pour gérer les rôles :

function autoriser(...roles) {
  return (req, res, next) => {
    if (!req.utilisateur) {
      return res.status(401).json({ message: 'Non authentifié' });
    }
    
    if (!roles.includes(req.utilisateur.role)) {
      return res.status(403).json({ message: 'Accès interdit' });
    }
    
    next();
  };
}

// Utilisation
router.delete('/utilisateurs/:id', 
  authentifier, 
  autoriser('admin', 'superadmin'),
  supprimerUtilisateur
);

Les erreurs de sécurité à éviter

Stocker le JWT en localStorage : vulnérable aux attaques XSS. Préférer les cookies HttpOnly pour les apps web.

Un secret JWT faible : le secret doit être long et aléatoire. En développement, j'utilise openssl rand -base64 64 pour générer un secret robuste.

Faire confiance au payload sans vérification : toujours vérifier la signature, même si tu viens de créer le token.

Ne pas gérer les erreurs de vérification : jwt.verify peut lancer plusieurs types d'erreurs (JsonWebTokenError, TokenExpiredError, NotBeforeError). Gérer chacune séparément donne de meilleures réponses au client.

Tester l'authentification

Un test simple avec Jest et supertest pour vérifier que le middleware fonctionne :

describe('Middleware authentifier', () => {
  it('refuse les requêtes sans token', async () => {
    const res = await request(app).get('/api/profil');
    expect(res.status).toBe(401);
  });

  it('accepte un token valide', async () => {
    const token = genererToken({ userId: 1, email: '[email protected]' });
    const res = await request(app)
      .get('/api/profil')
      .set('Authorization', `Bearer ${token}`);
    expect(res.status).toBe(200);
  });
});

Ce qu'on retient

JWT n'est pas une solution magique. Pour des applications web classiques avec sessions côté serveur, une authentification par cookies de session reste souvent plus simple. JWT brille vraiment pour les APIs consommées par des clients mobiles, des SPAs sur des domaines différents, ou des architectures microservices.

La règle d'or : access tokens courts, refresh tokens révocables, jamais d'informations sensibles dans le payload.