JWT et Interceptors Angular : Authentification sécurisée

🏷️ Front-end 📅 12/04/2026 03:00:00 👤 Mezgani Said
Angular Jwt Authentification Interceptor Security
JWT et Interceptors Angular : Authentification sécurisée

Maîtriser les JWT (JSON Web Tokens) avec les interceptors Angular pour gérer l'authentification, ajouter les tokens aux requêtes et gérer les erreurs 401.

Qu'est-ce que JWT et pourquoi l'utiliser ?

Un JWT (JSON Web Token) est un standard open (RFC 7519) pour créer des tokens d'authentification compacts et auto-suffisants. Contrairement aux sessions serveur, les JWT permettent une authentification stateless : le serveur n'a pas besoin de stocker les sessions en mémoire ou en cache.

À retenir : JWT remplace les sessions traditionnelles en encodant les informations de l'utilisateur directement dans le token, signé avec une clé secrète du serveur. L'API vérifie la signature pour s'assurer que le token n'a pas été falsifié.

Avantages des JWT :

  • Stateless : aucune session stockée sur le serveur, idéal pour les microservices et les APIs distribuées
  • Scalabilité : chaque serveur peut vérifier le token indépendamment
  • CORS : fonctionne parfaitement avec les requêtes cross-origin
  • Mobiles : facile à stocker localement sur des apps mobiles
  • Compact : peut être envoyé via URL, header ou body
// Exemple : Token JWT complet
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Ce token se divise en 3 parties séparées par des points : header.payload.signature.

Structure d'un JWT : header, payload et signature

Comprenons chaque partie du JWT :

1. Header (En-tête) : Contient des métadonnées sur le token.

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

2. Payload (Charge utile) : Contient les données (claims) de l'utilisateur.

{
  "sub": "user_id_123",
  "email": "user@example.com",
  "name": "John Doe",
  "roles": ["admin", "user"],
  "iat": 1516239022,
  "exp": 1516325422
}

3. Signature : Vérifie l'intégrité du token en utilisant la clé secrète.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)
À retenir : Les claims importants sont iat (émission), exp (expiration) et sub (sujet = user_id). Définissez exp de 15 minutes à 1 heure pour une meilleure sécurité.

Créer un service d'authentification

Le service d'authentification gère le login, le token storage et la vérification de l'utilisateur.

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

export interface User {
  id: string;
  email: string;
  name: string;
  roles: string[];
}

export interface AuthResponse {
  access_token: string;
  refresh_token: string;
  user: User;
}

@Injectable({ providedIn: 'root' })
export class AuthService {
  private readonly TOKEN_KEY = 'access_token';
  private readonly REFRESH_TOKEN_KEY = 'refresh_token';
  private readonly USER_KEY = 'user';

  private userSubject = new BehaviorSubject<User | null>(this.getUserFromStorage());
  public user$ = this.userSubject.asObservable();

  constructor(private http: HttpClient) {}

  login(email: string, password: string): Observable<AuthResponse> {
    return this.http.post<AuthResponse>('/api/auth/login', { email, password })
      .pipe(
        tap(response => this.storeTokens(response))
      );
  }

  logout(): void {
    localStorage.removeItem(this.TOKEN_KEY);
    localStorage.removeItem(this.REFRESH_TOKEN_KEY);
    localStorage.removeItem(this.USER_KEY);
    this.userSubject.next(null);
  }

  getToken(): string | null {
    return localStorage.getItem(this.TOKEN_KEY);
  }

  getRefreshToken(): string | null {
    return localStorage.getItem(this.REFRESH_TOKEN_KEY);
  }

  isAuthenticated(): boolean {
    return !!this.getToken();
  }

  private storeTokens(response: AuthResponse): void {
    localStorage.setItem(this.TOKEN_KEY, response.access_token);
    localStorage.setItem(this.REFRESH_TOKEN_KEY, response.refresh_token);
    localStorage.setItem(this.USER_KEY, JSON.stringify(response.user));
    this.userSubject.next(response.user);
  }

  private getUserFromStorage(): User | null {
    const user = localStorage.getItem(this.USER_KEY);
    return user ? JSON.parse(user) : null;
  }
}

Implémenter un interceptor HTTP

L'interceptor Angular ajoute automatiquement le JWT à chaque requête HTTP en header Authorization.

// jwt.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
  HttpEvent
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    // Ne pas ajouter le token aux requêtes d'authentification
    if (req.url.includes('/api/auth/login') ||
        req.url.includes('/api/auth/register')) {
      return next.handle(req);
    }

    const token = this.authService.getToken();
    if (token) {
      req = req.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      });
    }

    return next.handle(req);
  }
}

Enregistrez l'interceptor dans votre module :

// app.module.ts
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { JwtInterceptor } from './interceptors/jwt.interceptor';

@NgModule({
  imports: [HttpClientModule],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: JwtInterceptor,
      multi: true
    }
  ]
})
export class AppModule {}
Note : L'option multi: true permet d'enregistrer plusieurs interceptors. Ils s'exécutent dans l'ordre d'enregistrement.

Gérer les erreurs 401 et les tokens expirés

Créez un interceptor qui gère les réponses d'erreur, notamment les 401 (Unauthorized) quand le token a expiré.

// error.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AuthService } from './auth.service';
import { Router } from '@angular/router';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          // Token expiré ou invalide
          this.authService.logout();
          this.router.navigate(['/login']);
        }
        return throwError(() => error);
      })
    );
  }
}
À retenir : Un 401 signifie que le token est expiré ou invalide. Redirigez l'utilisateur vers la page de login et supprimez ses tokens stockés.

Refresh token : renouveler l'authentification

Anti-pattern courant : attendre juste le token d'accès et renvoyer l'utilisateur au login. Mieux : utiliser un refresh token pour obtenir automatiquement un nouveau access token.

// refresh-token.interceptor.ts
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 RefreshTokenInterceptor implements HttpInterceptor {
  private isRefreshing = false;
  private refreshTokenSubject = new BehaviorSubject<string | null>(null);

  constructor(private authService: AuthService) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401 && !this.isRefreshing) {
          this.isRefreshing = true;
          this.refreshTokenSubject.next(null);

          const refreshToken = this.authService.getRefreshToken();
          if (refreshToken) {
            return this.authService.refreshAccessToken(refreshToken)
              .pipe(
                switchMap((response: any) => {
                  this.isRefreshing = false;
                  this.refreshTokenSubject.next(response.access_token);
                  return next.handle(this.addToken(req, response.access_token));
                }),
                catchError(() => {
                  this.isRefreshing = false;
                  this.authService.logout();
                  return throwError(() => error);
                })
              );
          }
        }

        if (this.isRefreshing) {
          return this.refreshTokenSubject.pipe(
            filter(token => token != null),
            take(1),
            switchMap(token => next.handle(this.addToken(req, token!)))
          );
        }

        return throwError(() => error);
      })
    );
  }

  private addToken(req: HttpRequest<any>, token: string): HttpRequest<any> {
    return req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
  }
}

Ajoutez la méthode refreshAccessToken dans le service :

// auth.service.ts (ajout)
refreshAccessToken(refreshToken: string): Observable<AuthResponse> {
  return this.http.post<AuthResponse>('/api/auth/refresh', { refresh_token: refreshToken });
}

Bonnes pratiques et sécurité

1. Expiration courte de l'access token

Fixez l'expiration à 15 minutes maximum. Cela limite les dégâts en cas de token volé. L'utilisateur ne verra pas la différence car le refresh token lui permet de renouveler silencieusement.

2. Stockage sécurisé

Ne stockez jamais les tokens dans le localStorage pour les applis sensibles. Préférez :

  • HttpOnly cookies : plus sécurisé contre XSS, mais vulnérable à CSRF. À utiliser avec des tokens CSRF.
  • Session memory : perdu au rechargement, mais très sûr. À combiner avec un refresh token en HttpOnly cookie.
  • localStorage : pratique pour les PWA, mais vulnérable à XSS. Sanitisez toujours vos inputs.

3. Signature HMAC vs RSA

  • HS256 (HMAC-SHA256) : clé secrète partagée entre client et serveur. Simple, rapide, idéal pour les monolithes.
  • RS256 (RSA) : clé privée/publique. Le serveur signe avec la clé privée, les clients vérifient avec la clé publique. Meilleur pour les microservices.

4. Claim exp obligatoire

Vérifiez toujours que le claim exp n'a pas expiré avant de laisser passer la requête.

5. CORS et SameSite

Si vous utilisez les tokens en headers (plutôt que cookies), CORS fonctionne sans problème. Mais si vous utilisez des cookies HttpOnly, configurez SameSite=Strict pour éviter les attaques CSRF.

6. Rotation du refresh token

Changez le refresh token à chaque renouvellement. Cela limite la fenêtre d'exploitation si un ancien token est volé.

À retenir : JWT n'est pas une cure-miracle de sécurité. Combinez-le avec HTTPS, CORS rigoreux, validation côté serveur et sanitisation des inputs.

Contrôle de liste de révocation (blacklist)

// Exemple : marquer un token comme révoqué au logout
// Dans le backend, stockez les tokens révoqués en Redis ou BD
// Exemple: token_blacklist: { token_jti -> expiration }

// Vérifiez avant d'accepter la requête
if (token.jti in blacklist) {
  reject("Token has been revoked");
}