Back-end angularforall.com

- JWT : authentifier une API Node.js/Angular

Jwt Nodejs Authentication Express Angular Token Api Security
JWT : authentifier une API Node.js/Angular

Authentifiez une API Node.js avec JWT : tokens, middleware, refresh tokens, intégration Angular et sécurité production.

1. Prérequis et configuration initiale

Avant de commencer, assurez-vous d'avoir :

  • Node.js 16+ (LTS recommandé)
  • npm ou yarn pour les dépendances
  • Express.js pour le serveur HTTP
  • MongoDB ou PostgreSQL (optionnel, pour la base utilisateurs)
  • Angular 15+ côté client

Installation des dépendances

npm install express dotenv jsonwebtoken bcryptjs cors mongoose
# ou avec yarn
yarn add express dotenv jsonwebtoken bcryptjs cors mongoose

Fichier .env (ne pas commiter)

# Variables secrètes - JAMAIS en production sans vault
PORT=5000
NODE_ENV=development
DB_URI=mongodb://localhost:27017/auth-api
JWT_SECRET=votre_clé_secrète_très_longue_128_caractères_minimum_générée_aléatoirement
JWT_REFRESH_SECRET=votre_clé_refresh_différente_et_secrète
JWT_EXPIRE=15m
JWT_REFRESH_EXPIRE=7d
CORS_ORIGIN=http://localhost:4200

Structure de projet

auth-api/
├── config/
│   └── database.js           # Configuration MongoDB/Prisma
├── controllers/
│   └── authController.js     # Login, register, refresh
├── middlewares/
│   ├── authMiddleware.js     # Validation JWT
│   └── errorHandler.js       # Gestion centralisée des erreurs
├── models/
│   └── User.js               # Schéma utilisateur
├── routes/
│   ├── auth.js               # Routes /auth
│   └── protected.js          # Routes protégées
├── app.js                    # Configuration Express
├── server.js                 # Entry point
├── .env                      # Variables d'env (git ignored)
└── .env.example              # Exemple pour documentation

2. Concepts fondamentaux de JWT

Un JWT est composé de 3 parties séparées par des points :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header

Métadonnées du token (algorithme, type)

{
  "alg": "HS256",
  "typ": "JWT"
}
Payload

Données utilisateur (claims)

{
  "userId": "60d5ec",
  "email": "user@ex.fr",
  "role": "admin",
  "iat": 1672500000,
  "exp": 1672503600
}
Signature

Garantit l'intégrité du token

HMACSHA256(
  base64(header) + "." +
  base64(payload),
  SECRET_KEY
)

Avantages de JWT

  • Stateless : Pas besoin de stocker les sessions côté serveur
  • Scalable : Fonctionne facilement avec les microservices
  • Mobile-friendly : Parfait pour les applications mobiles
  • Auto-validé : Signature garantit l'intégrité du payload

JWT vs Sessions traditionnelles

Critère Sessions JWT
Stockage côté serveur ❌ Oui (mémoire/Redis) ✅ Non (stateless)
Scalabilité horizontale ❌ Compliquée (session affinity) ✅ Native
Mobile/CORS ⚠️ Problèmes de cookies ✅ Header Authorization
Révocation de token ✅ Instantanée ⚠️ Besoin d'une blacklist Redis
Taille de requête ✅ Petite (ID session) ⚠️ Plus grand (payload complet)

3. Générer un JWT en Node.js

Configuration de base (app.js)

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const app = express();

// Middlewares globaux
app.use(express.json());
app.use(cors({
    origin: process.env.CORS_ORIGIN || 'http://localhost:4200',
    credentials: true // Autoriser les cookies si nécessaire
}));

// Routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/protected', require('./routes/protected'));

// Gestion des erreurs globale
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(err.status || 500).json({
        success: false,
        message: err.message || 'Erreur serveur',
        error: process.env.NODE_ENV === 'development' ? err : {}
    });
});

module.exports = app;

Service JWT (utils/jwt.js)

const jwt = require('jsonwebtoken');

/**
 * Génère un access token JWT avec payload utilisateur
 * @param {Object} payload - Données à encoder (userId, role, etc.)
 * @param {string} type - 'access' ou 'refresh'
 * @returns {string} Token signé
 */
function generateToken(payload, type = 'access') {
    const secret = type === 'access'
        ? process.env.JWT_SECRET
        : process.env.JWT_REFRESH_SECRET;

    const expiresIn = type === 'access'
        ? process.env.JWT_EXPIRE
        : process.env.JWT_REFRESH_EXPIRE;

    return jwt.sign(payload, secret, {
        expiresIn,
        algorithm: 'HS256' // Explicite : HMAC-SHA256
    });
}

/**
 * Vérifie la signature et décode le token
 * @param {string} token - JWT à valider
 * @param {string} type - 'access' ou 'refresh'
 * @returns {Object} Payload décodé
 * @throws {Error} Si token invalide ou expiré
 */
function verifyToken(token, type = 'access') {
    const secret = type === 'access'
        ? process.env.JWT_SECRET
        : process.env.JWT_REFRESH_SECRET;

    try {
        return jwt.verify(token, secret, { algorithms: ['HS256'] });
    } catch (error) {
        if (error.name === 'TokenExpiredError') {
            throw new Error('Token expiré');
        }
        if (error.name === 'JsonWebTokenError') {
            throw new Error('Token invalide');
        }
        throw error;
    }
}

module.exports = { generateToken, verifyToken };

Controller : Login et Registration

const bcrypt = require('bcryptjs');
const { generateToken } = require('../utils/jwt');
const User = require('../models/User');

/**
 * POST /api/auth/register
 * Crée un nouvel utilisateur avec mot de passe hashé
 */
exports.register = async (req, res, next) => {
    try {
        const { email, password, firstName, lastName } = req.body;

        // Validation entrée utilisateur
        if (!email || !password) {
            return res.status(400).json({
                success: false,
                message: 'Email et mot de passe requis'
            });
        }

        if (password.length < 8) {
            return res.status(400).json({
                success: false,
                message: 'Mot de passe minimum 8 caractères'
            });
        }

        // Vérifier si utilisateur existe déjà
        const existingUser = await User.findOne({ email });
        if (existingUser) {
            return res.status(409).json({
                success: false,
                message: 'Email déjà utilisé'
            });
        }

        // Hash du mot de passe avec salt rounds
        const salt = await bcrypt.genSalt(10); // 10 iterations
        const hashedPassword = await bcrypt.hash(password, salt);

        // Créer utilisateur
        const newUser = await User.create({
            email,
            firstName,
            lastName,
            password: hashedPassword,
            role: 'user' // Rôle par défaut
        });

        // Générer tokens
        const accessToken = generateToken({
            userId: newUser._id,
            email: newUser.email,
            role: newUser.role
        }, 'access');

        const refreshToken = generateToken({
            userId: newUser._id
        }, 'refresh');

        // Sauvegarder refresh token (optionnel : base de données)
        newUser.refreshToken = refreshToken;
        await newUser.save();

        res.status(201).json({
            success: true,
            message: 'Utilisateur créé avec succès',
            accessToken,
            refreshToken,
            user: {
                id: newUser._id,
                email: newUser.email,
                firstName: newUser.firstName
            }
        });
    } catch (error) {
        next(error);
    }
};

/**
 * POST /api/auth/login
 * Authentifie un utilisateur et retourne les tokens
 */
exports.login = async (req, res, next) => {
    try {
        const { email, password } = req.body;

        // Validation
        if (!email || !password) {
            return res.status(400).json({
                success: false,
                message: 'Email et mot de passe requis'
            });
        }

        // Trouver l'utilisateur
        const user = await User.findOne({ email });
        if (!user) {
            return res.status(401).json({
                success: false,
                message: 'Email ou mot de passe incorrect'
            });
        }

        // Comparer mot de passe
        const isPasswordValid = await bcrypt.compare(password, user.password);
        if (!isPasswordValid) {
            return res.status(401).json({
                success: false,
                message: 'Email ou mot de passe incorrect'
            });
        }

        // Générer tokens
        const accessToken = generateToken({
            userId: user._id,
            email: user.email,
            role: user.role
        }, 'access');

        const refreshToken = generateToken({
            userId: user._id
        }, 'refresh');

        // Sauvegarder refresh token
        user.refreshToken = refreshToken;
        await user.save();

        res.status(200).json({
            success: true,
            message: 'Connexion réussie',
            accessToken,
            refreshToken,
            user: {
                id: user._id,
                email: user.email,
                firstName: user.firstName,
                role: user.role
            }
        });
    } catch (error) {
        next(error);
    }
};

4. Valider JWT avec un middleware

Le middleware d'authentification intercepte chaque requête protégée et vérifie la signature du JWT.

Middleware d'authentification

const { verifyToken } = require('../utils/jwt');

/**
 * Middleware : Valide le JWT dans l'header Authorization
 * Stocke le payload décrypté dans req.user
 *
 * Format attendu : Authorization: Bearer <token>
 */
const authMiddleware = (req, res, next) => {
    try {
        // Extraire token de l'header
        const authHeader = req.headers.authorization;

        if (!authHeader || !authHeader.startsWith('Bearer ')) {
            return res.status(401).json({
                success: false,
                message: 'Token manquant ou format invalide'
            });
        }

        // Extraire token après "Bearer "
        const token = authHeader.slice(7);

        // Vérifier la signature
        const decoded = verifyToken(token, 'access');

        // Attacher les données utilisateur à la requête
        req.user = decoded;

        next();
    } catch (error) {
        return res.status(401).json({
            success: false,
            message: error.message || 'Token invalide'
        });
    }
};

/**
 * Middleware : Vérifie le rôle de l'utilisateur
 * À utiliser après authMiddleware
 *
 * Usage : app.get('/admin', authMiddleware, roleMiddleware(['admin']), controller);
 */
const roleMiddleware = (allowedRoles) => {
    return (req, res, next) => {
        // req.user est défini par authMiddleware
        if (!req.user) {
            return res.status(401).json({
                success: false,
                message: 'Non authentifié'
            });
        }

        // Vérifier si le rôle utilisateur est autorisé
        if (!allowedRoles.includes(req.user.role)) {
            return res.status(403).json({
                success: false,
                message: `Accès refusé. Rôles autorisés: ${allowedRoles.join(', ')}`
            });
        }

        next();
    };
};

module.exports = { authMiddleware, roleMiddleware };

Utilisation dans les routes

const express = require('express');
const { authMiddleware, roleMiddleware } = require('../middlewares/authMiddleware');

const router = express.Router();

/**
 * GET /api/protected/profile
 * Route protégée : nécessite un JWT valide
 */
router.get('/profile', authMiddleware, (req, res) => {
    // req.user contient l'utilisateur décodé
    res.json({
        success: true,
        message: 'Profil utilisateur',
        user: req.user
    });
});

/**
 * GET /api/protected/admin
 * Route réservée aux administrateurs
 */
router.get('/admin/users',
    authMiddleware,
    roleMiddleware(['admin']),
    (req, res) => {
        // Seul les admins peuvent accéder
        res.json({
            success: true,
            message: 'Liste des utilisateurs',
            // Requête base de données ici
        });
    }
);

/**
 * GET /api/protected/dashboard
 * Route accessible aux users et admins
 */
router.get('/dashboard',
    authMiddleware,
    roleMiddleware(['user', 'admin']),
    (req, res) => {
        res.json({
            success: true,
            message: 'Tableau de bord',
            userId: req.user.userId
        });
    }
);

module.exports = router;
💡 Conseil : Utilisez toujours authMiddleware avant roleMiddleware. L'ordre des middlewares dans Express détermine l'ordre d'exécution.

5. Implémenter les refresh tokens

Problème : Les access tokens expireront rapidement (15 min). Si on les rend durables, on risque qu'un token volé soit utilisé longtemps.

Solution : Utiliser des refresh tokens (longue durée) pour régénérer des access tokens sans redemander le mot de passe.

Access Token (15 min)
  • ✅ Courte durée
  • ✅ Utilisé à chaque requête
  • ⚠️ Si volé, dégâts limités
Refresh Token (1 jours)
  • ✅ Longue durée
  • ✅ Stocké de façon sécurisée
  • ⚠️ Si volé, pirate peut obtenir nouveaux access tokens

Controller : Refresh Token

/**
 * POST /api/auth/refresh
 * Génère un nouvel access token à partir du refresh token
 */
exports.refreshAccessToken = async (req, res, next) => {
    try {
        const { refreshToken } = req.body;

        if (!refreshToken) {
            return res.status(400).json({
                success: false,
                message: 'Refresh token requis'
            });
        }

        // Vérifier la signature du refresh token
        const decoded = verifyToken(refreshToken, 'refresh');

        // Vérifier que l'utilisateur existe toujours
        const user = await User.findById(decoded.userId);
        if (!user) {
            return res.status(401).json({
                success: false,
                message: 'Utilisateur non trouvé'
            });
        }

        // Vérifier que le refresh token stocké correspond
        // (protection contre les tokens volés après logout)
        if (user.refreshToken !== refreshToken) {
            return res.status(401).json({
                success: false,
                message: 'Refresh token invalide ou révoqué'
            });
        }

        // Générer nouvel access token
        const newAccessToken = generateToken({
            userId: user._id,
            email: user.email,
            role: user.role
        }, 'access');

        res.json({
            success: true,
            accessToken: newAccessToken
        });
    } catch (error) {
        return res.status(401).json({
            success: false,
            message: 'Refresh token expiré ou invalide'
        });
    }
};

/**
 * POST /api/auth/logout
 * Révoque le refresh token (logout)
 */
exports.logout = async (req, res, next) => {
    try {
        // Récupérer l'utilisateur depuis le token d'accès
        const userId = req.user.userId; // Vient de authMiddleware

        // Supprimer/invalider le refresh token côté serveur
        await User.findByIdAndUpdate(userId, { refreshToken: null });

        res.json({
            success: true,
            message: 'Déconnexion réussie'
        });
    } catch (error) {
        next(error);
    }
};

Route de refresh

// Dans routes/auth.js
const express = require('express');
const { login, register, refreshAccessToken, logout } = require('../controllers/authController');
const { authMiddleware } = require('../middlewares/authMiddleware');

const router = express.Router();

router.post('/login', login);
router.post('/register', register);
router.post('/refresh', refreshAccessToken); // Public
router.post('/logout', authMiddleware, logout); // Protégé

module.exports = router;

Cycle de vie des tokens

1. LOGIN
   Client: POST /auth/login { email, password }
   Serveur: Valide credentials → génère accessToken + refreshToken
   Client: Stocke accessToken (localStorage) + refreshToken (httpOnly cookie ou localStorage)

2. REQUÊTE PROTÉGÉE
   Client: GET /protected/profile + Header: Authorization: Bearer <accessToken>
   Serveur: Valide token → Répond avec données

3. ACCESS TOKEN EXPIRE (après 15 min)
   Client: Reçoit 401 Unauthorized
   Client: POST /auth/refresh { refreshToken }
   Serveur: Valide refreshToken → Génère nouvel accessToken
   Client: Relance requête initiale avec nouvel accessToken

4. REFRESH TOKEN EXPIRE (après 7 jours)
   Client: POST /auth/refresh { refreshToken }
   Serveur: Retourne 401 (refreshToken expiré)
   Client: Redirige vers login

5. LOGOUT
   Client: POST /auth/logout + Header: Authorization: Bearer <accessToken>
   Serveur: Invalide refreshToken → Répond 200
   Client: Supprime tokens locaux

6. Intégration avec Angular HttpClient

Angular utilise HttpInterceptors pour ajouter le JWT automatiquement à chaque requête HTTP.

Service d'authentification (auth.service.ts)

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

interface AuthResponse {
  success: boolean;
  accessToken: string;
  refreshToken: string;
  user: any;
}

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private apiUrl = 'http://localhost:5000/api/auth';

  // Observable pour l'état d'authentification
  private isAuthenticatedSubject = new BehaviorSubject(this.hasToken());
  public isAuthenticated$ = this.isAuthenticatedSubject.asObservable();

  constructor(private http: HttpClient) {}

  /**
   * Enregistrement d'un nouvel utilisateur
   */
  register(email: string, password: string, firstName: string): Observable {
    return this.http.post(`${this.apiUrl}/register`, {
      email,
      password,
      firstName
    }).pipe(
      tap(response => {
        // Sauvegarder les tokens
        this.setTokens(response.accessToken, response.refreshToken);
        this.isAuthenticatedSubject.next(true);
      })
    );
  }

  /**
   * Connexion utilisateur
   */
  login(email: string, password: string): Observable {
    return this.http.post(`${this.apiUrl}/login`, {
      email,
      password
    }).pipe(
      tap(response => {
        this.setTokens(response.accessToken, response.refreshToken);
        this.isAuthenticatedSubject.next(true);
      })
    );
  }

  /**
   * Récupère l'access token stocké
   */
  getAccessToken(): string | null {
    return localStorage.getItem('accessToken');
  }

  /**
   * Récupère le refresh token stocké
   */
  getRefreshToken(): string | null {
    return localStorage.getItem('refreshToken');
  }

  /**
   * Sauvegarde les tokens dans localStorage
   */
  private setTokens(accessToken: string, refreshToken: string): void {
    localStorage.setItem('accessToken', accessToken);
    localStorage.setItem('refreshToken', refreshToken);
  }

  /**
   * Supprime les tokens et déconnecte l'utilisateur
   */
  logout(): Observable {
    return this.http.post(`${this.apiUrl}/logout`, {}).pipe(
      tap(() => {
        localStorage.removeItem('accessToken');
        localStorage.removeItem('refreshToken');
        this.isAuthenticatedSubject.next(false);
      })
    );
  }

  /**
   * Vérifie si un token valide existe
   */
  private hasToken(): boolean {
    const token = localStorage.getItem('accessToken');
    if (!token) return false;

    // Vérifier que le token n'est pas expiré
    try {
      const payload = this.decodeToken(token);
      const expirationTime = payload.exp * 1000; // exp est en secondes
      return Date.now() < expirationTime;
    } catch {
      return false;
    }
  }

  /**
   * Décode le JWT sans vérifier la signature (lecture côté client)
   */
  private decodeToken(token: string): any {
    const parts = token.split('.');
    if (parts.length !== 3) return null;

    const decoded = atob(parts[1]); // Décode le payload (partie 2)
    return JSON.parse(decoded);
  }

  /**
   * Régénère un access token avec le refresh token
   */
  refreshAccessToken(): Observable<{ accessToken: string }> {
    const refreshToken = this.getRefreshToken();
    return this.http.post<{ accessToken: string }>(
      `${this.apiUrl}/refresh`,
      { refreshToken }
    ).pipe(
      tap(response => {
        localStorage.setItem('accessToken', response.accessToken);
      })
    );
  }
}

HTTP Interceptor pour le JWT

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError, BehaviorSubject } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';
import { AuthService } from './auth.service';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
  // Subject pour éviter les appels refresh multiples simultanés
  private isRefreshing = false;
  private refreshTokenSubject: BehaviorSubject = new BehaviorSubject(null);

  constructor(private authService: AuthService) {}

  intercept(request: HttpRequest, next: HttpHandler): Observable> {
    // Ajouter le JWT à toutes les requêtes (sauf login/register)
    const token = this.authService.getAccessToken();
    if (token && this.isNonAuthRequest(request)) {
      request = this.addToken(request, token);
    }

    return next.handle(request).pipe(
      catchError(error => {
        if (error instanceof HttpErrorResponse && error.status === 401) {
          return this.handle401Error(request, next);
        }
        return throwError(() => error);
      })
    );
  }

  /**
   * Ajoute le JWT au header Authorization
   */
  private addToken(request: HttpRequest, token: string): HttpRequest {
    return request.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
  }

  /**
   * Traite les erreurs 401 : refresh le token et relance la requête
   */
  private handle401Error(request: HttpRequest, next: HttpHandler): Observable> {
    if (!this.isRefreshing) {
      this.isRefreshing = true;
      this.refreshTokenSubject.next(null);

      return this.authService.refreshAccessToken().pipe(
        switchMap(response => {
          this.isRefreshing = false;
          this.refreshTokenSubject.next(response.accessToken);
          return next.handle(this.addToken(request, response.accessToken));
        }),
        catchError(error => {
          // Refresh token expiré → rediriger vers login
          this.authService.logout().subscribe();
          return throwError(() => error);
        })
      );
    } else {
      // Attendre que le refresh précédent se termine
      return this.refreshTokenSubject.pipe(
        filter(token => token != null),
        take(1),
        switchMap(token => {
          return next.handle(this.addToken(request, token));
        })
      );
    }
  }

  /**
   * Vérifie si c'est une requête qui ne nécessite pas le JWT
   */
  private isNonAuthRequest(request: HttpRequest): boolean {
    return !request.url.includes('/login') && !request.url.includes('/register');
  }
}

Fournir l'interceptor (app.config.ts - Angular 14+)

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors, HTTP_INTERCEPTORS } from '@angular/common/http';
import { JwtInterceptor } from './interceptors/jwt.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([]) // Utiliser le nouveau système d'interceptors si disponible
    ),
    // Ou utiliser l'ancienne approche avec HTTP_INTERCEPTORS
    { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }
  ]
};

Utilisation dans un composant

import { Component } from '@angular/core';
import { AuthService } from './services/auth.service';

@Component({
  selector: 'app-login',
  template: `
    <form (ngSubmit)="onLogin()">
      <input [(ngModel)]="email" name="email" placeholder="Email" required>
      <input [(ngModel)]="password" name="password" type="password" placeholder="Password" required>
      <button type="submit" [disabled]="loading">Connexion</button>
      <p *ngIf="error" class="text-danger">{{ error }}</p>
    </form>
  `
})
export class LoginComponent {
  email = '';
  password = '';
  loading = false;
  error = '';

  constructor(private authService: AuthService) {}

  onLogin(): void {
    this.loading = true;
    this.error = '';

    // Le JWT est automatiquement ajouté par l'interceptor
    this.authService.login(this.email, this.password).subscribe({
      next: (response) => {
        console.log('Connexion réussie', response.user);
        // Rediriger vers dashboard
      },
      error: (err) => {
        this.error = err.error?.message || 'Erreur de connexion';
        this.loading = false;
      }
    });
  }
}

7. Sécurité et bonnes pratiques

🔐 Secrets et clés

  • Générez des clés longues : Minimum 128 bits (32 hex chars)
  • Utilisez des clés différentes : JWT_SECRET ≠ JWT_REFRESH_SECRET
  • Stockez dans variables d'environnement : Jamais en dur dans le code
  • En production : Utilisez un vault (HashiCorp Vault, AWS Secrets Manager)

🔑 Stockage des tokens (côté Angular)

Stockage Avantages Risques
localStorage Persistent, simple XSS : JS peut voler le token
sessionStorage Moins persistant que localStorage XSS toujours possible, perte au refresh
httpOnly Cookie JS ne peut pas accéder (XSS safe) CSRF : compliqué avec CORS/mobile
Memory + Refresh Cookie XSS safe + CSRF protection Complexe, perte au refresh page

⚠️ Conseil : Pour les applications modernes, préférez httpOnly Secure Cookies + CSRF tokens. Si localStorage : sanitizez l'HTML (DomSanitizer Angular).

🛡️ Protection contre les attaques courantes

  • XSS (Cross-Site Scripting) : Utilisez httpOnly cookies, validez/échappez les inputs
  • CSRF (Cross-Site Request Forgery) : SameSite cookies, CSRF tokens
  • Token stealing : HTTPS obligatoire, Secure flag sur cookies
  • Brute force login : Rate limiting, exponential backoff
  • Token compromise : Refresh token blacklist, rotation de secrets

⏰ Temps d'expiration recommandés

  • Access Token : 15 minutes (compromis sécurité/UX)
  • Refresh Token : 7 jours (durée de session)
  • Après logout : Invalider immédiatement côté serveur

📋 Checklist de sécurité

8. Cas d'usage avancés en production

Rôles et permissions granulaires

// Améliorer le payload du JWT avec permissions
const accessToken = generateToken({
    userId: user._id,
    email: user.email,
    role: user.role,
    permissions: user.permissions, // ['users:read', 'posts:write', 'admin:delete']
    department: user.department // Pour audit/monitoring
}, 'access');

// Middleware de permission granulaire
const permissionMiddleware = (requiredPermissions) => {
    return (req, res, next) => {
        const userPermissions = req.user.permissions || [];
        const hasPermission = requiredPermissions.some(p => userPermissions.includes(p));

        if (!hasPermission) {
            return res.status(403).json({
                success: false,
                message: 'Permission insuffisante'
            });
        }
        next();
    };
};

Blacklist de tokens (révocation)

// Utiliser Redis pour une blacklist de tokens révoqués
const redis = require('redis');
const client = redis.createClient();

/**
 * Ajoute un token à la blacklist (logout ou sécurité)
 */
async function blacklistToken(token, expirationTime) {
    const ttl = Math.floor((expirationTime - Date.now()) / 1000);
    if (ttl > 0) {
        await client.setex(`blacklist:${token}`, ttl, 'revoked');
    }
}

/**
 * Vérifier si un token est blacklisté
 */
async function isTokenBlacklisted(token) {
    const result = await client.get(`blacklist:${token}`);
    return result !== null;
}

// Intégrer dans le middleware
const authMiddleware = async (req, res, next) => {
    try {
        const token = req.headers.authorization?.slice(7);
        if (!token) return res.status(401).json({ message: 'Token manquant' });

        // Vérifier blacklist
        if (await isTokenBlacklisted(token)) {
            return res.status(401).json({ message: 'Token révoqué' });
        }

        const decoded = verifyToken(token, 'access');
        req.user = decoded;
        next();
    } catch (error) {
        res.status(401).json({ message: 'Token invalide' });
    }
};

Rotation de secrets

// Supporter plusieurs clés secrètes pendant la rotation
const JWT_SECRETS = [
    process.env.JWT_SECRET_V2,    // Clé actuelle
    process.env.JWT_SECRET_V1     // Ancienne clé (30 jours après rotation)
];

function verifyTokenWithRotation(token, type = 'access') {
    const secrets = type === 'access' ? JWT_SECRETS : [process.env.JWT_REFRESH_SECRET];

    for (const secret of secrets) {
        try {
            return jwt.verify(token, secret, { algorithms: ['HS256'] });
        } catch (error) {
            continue; // Essayer la clé suivante
        }
    }

    throw new Error('Token invalide avec toutes les clés');
}

Token pair (Access + Refresh) sécurisé

/**
 * Stratégie : Access Token en mémoire + Refresh Token en httpOnly Cookie
 * = Protection XSS + CSRF
 */

// Côté Node.js : envoyer refresh token en httpOnly cookie
exports.login = async (req, res, next) => {
    try {
        const { email, password } = req.body;

        // Authentifier utilisateur...
        const user = await User.findOne({ email });
        const isValid = await bcrypt.compare(password, user.password);

        if (!isValid) {
            return res.status(401).json({ message: 'Credentials invalides' });
        }

        const accessToken = generateToken({ userId: user._id, role: user.role }, 'access');
        const refreshToken = generateToken({ userId: user._id }, 'refresh');

        // Envoyer refresh token en httpOnly Secure Cookie
        res.cookie('refreshToken', refreshToken, {
            httpOnly: true,        // JS ne peut pas accéder
            secure: process.env.NODE_ENV === 'production', // HTTPS seulement
            sameSite: 'Strict',    // CSRF protection
            maxAge: 7 * 24 * 60 * 60 * 1000 // 7 jours
        });

        // Access token en réponse JSON (Angular stocke en mémoire/sessionStorage)
        res.json({
            success: true,
            accessToken,
            user: { id: user._id, email: user.email }
        });
    } catch (error) {
        next(error);
    }
};

// Côté Angular : Refresh automatique via HttpOnly cookie
exports.refreshAccessToken = (req, res, next) => {
    try {
        const refreshToken = req.cookies.refreshToken; // Lire depuis cookie

        if (!refreshToken) {
            return res.status(401).json({ message: 'Refresh token manquant' });
        }

        const decoded = verifyToken(refreshToken, 'refresh');
        const newAccessToken = generateToken({
            userId: decoded.userId
        }, 'access');

        res.json({ success: true, accessToken: newAccessToken });
    } catch (error) {
        res.status(401).json({ message: 'Refresh token invalide' });
    }
};

Monitoring et audit

// Logger les tentatives d'authentification pour détecter les attaques
function logAuthEvent(event, userId, ip, userAgent, details = {}) {
    console.log(JSON.stringify({
        timestamp: new Date().toISOString(),
        event, // 'login', 'failed_login', 'token_refresh', 'logout'
        userId,
        ip: req.ip,
        userAgent,
        ...details
    }));
}

// Tracker les tentatives d'accès non autorisés
app.use((req, res, next) => {
    res.on('finish', () => {
        if (res.statusCode === 401 || res.statusCode === 403) {
            logAuthEvent('unauthorized_access', req.user?.userId || 'anonymous', req.ip, req.get('user-agent'), {
                path: req.path,
                method: req.method,
                statusCode: res.statusCode
            });
        }
    });
    next();
});

Conclusion et ressources

JWT est devenu le standard d'authentification pour les APIs modernes. Nous avons couvert :

Checklist avant déploiement production

  • ✅ Secrets stockés en variables d'environnement
  • ✅ HTTPS activé sur tous les endpoints
  • ✅ CORS restrictif (pas de wildcard *)
  • ✅ Rate limiting sur /login
  • ✅ Helmet.js pour les headers de sécurité
  • ✅ Validation et sanitization des inputs
  • ✅ Logs d'authentification centralisés
  • ✅ Tests d'intégration complets (login, refresh, 401/403)
  • ✅ Monitoring des tokens révoqués/expirés
  • ✅ Backup et rotation régulière des secrets

Ressources officielles

Prochaines étapes : Implémentez OAuth2 pour les connexions tierces (Google, GitHub), ajoutez la 2FA, et monitez les anomalies d'authentification avec des alertes.

Conclusion

JWT est une technologie puissante pour sécuriser les APIs modernes. En combinant les bonnes pratiques (secrets sécurisés, expiration courte, refresh tokens, HTTPS), vous construisez un système d'authentification robuste et scalable.

Résumé des points clés :

  • JWT = Token stateless idéal pour les microservices
  • Générer et valider avec jsonwebtoken npm
  • Middleware d'authentification sur les routes protégées
  • Refresh tokens pour prolonger les sessions en toute sécurité
  • HttpInterceptor Angular pour envoyer le token automatiquement
  • HTTPS, secrets d'environnement, CORS restrictif en production

Partager