← Retour au blog
jwtangularauthenticationguardssecurity

JWT et Guards Angular : implémenter l'authentification sans pièges de sécurité

L'authentification JWT en Angular est devenue un standard incontournable, mais beaucoup de développeurs la mettent en œuvre de manière fragile. Le pattern classique—stocker le token en localStorage, le joindre aux requêtes via un interceptor—fonctionne, certes, mais laisse des brèches. Entre la gestion des tokens expirés, la synchronisation entre onglets, et les fuites XSS, il y a de quoi perdre du sommeil en production. Cet article propose une approche pragmatique : un système d'authentification robuste avec des guards qui reflètent vraiment l'état de l'utilisateur, sans sur-ingénierie.

Stocker le JWT : localStorage, sessionStorage ou memory ?

Le choix du stockage du token détermine votre surface d'attaque. localStorage persiste entre les sessions mais reste vulnérable aux attaques XSS ; un script malveillant peut le voler en une ligne : localStorage.getItem('token') . sessionStorage est plus sûr (détruit à la fermeture de l'onglet) mais moins pratique pour le multi-onglets. La vraie solution ? Utiliser un httpOnly cookie côté serveur, que le navigateur envoie automatiquement sans que JavaScript n'y accède. Malheureusement, beaucoup d'APIs publiques ne supportent que les tokens Bearer en header Authorization. Dans ce cas, combinez localStorage pour le refresh token (à faible durée de vie du access token) avec un service Angular qui centralise l'accès au token. Évitez à tout prix de l'exposer globalement : ne mettez jamais le token en variable globale ou en propriété publique d'un service. Encapsulez-le dans une propriété privée avec un getter sécurisé.

Architecture du service d'authentification

Un bon service d'authentification isole la logique métier du stockage. Créez une classe AuthService qui gère l'obtention du token, son renouvellement, et sa destruction. Le service doit exposer un Observable ou un Signal de l'état d'authentification, pas le token lui-même. Utilisez un BehaviorSubject pour que tous les composants abonnés réagissent immédiatement aux changements d'état. Voici une structure minimaliste : définissez une méthode login(credentials) qui appelle l'API, stocke le token de manière sécurisée, et met à jour l'état interne. Ajoutez une méthode refreshToken() qui renouvelle silencieusement le token avant expiration. Implementez logout() qui détruit le token et réinitialise l'état. Crucially, le service doit distinguer entre un utilisateur authentifié (token valide en mémoire) et un utilisateur reconnu (cookie valide côté serveur après un refresh de page). Cette distinction est clé pour éviter les redirections inutiles.

export class AuthService {
  private readonly tokenKey = 'access_token';
  private authState$ = new BehaviorSubject<AuthState>({ isAuthenticated: false });
  
  constructor(private http: HttpClient) {
    this.checkAuth();
  }
  
  login(credentials: { email: string; password: string }) {
    return this.http.post<{ token: string }>('/api/auth/login', credentials)
      .pipe(
        tap(res => {
          this.storeToken(res.token);
          this.authState$.next({ isAuthenticated: true, user: this.decodeToken(res.token) });
        })
      );
  }
  
  refreshToken() {
    return this.http.post<{ token: string }>('/api/auth/refresh', {})
      .pipe(
        tap(res => this.storeToken(res.token)),
        catchError(() => {
          this.logout();
          return throwError(() => new Error('Refresh failed'));
        })
      );
  }
  
  private storeToken(token: string) {
    localStorage.setItem(this.tokenKey, token);
  }
  
  getToken(): string | null {
    return localStorage.getItem(this.tokenKey);
  }
  
  logout() {
    localStorage.removeItem(this.tokenKey);
    this.authState$.next({ isAuthenticated: false });
  }
  
  isAuthenticated$() {
    return this.authState$.asObservable().pipe(map(state => state.isAuthenticated));
  }
}

Guards et redirection intelligente

Les guards Angular (CanActivate, CanActivateChild, CanDeactivate) sont votre première ligne de défense contre les accès non autorisés. Un guard doit être synchrone si possible—une vérification en mémoire de l'état d'authentification—pour éviter les tremblements visuels (flicker). Retournez un boolean ou un UrlTree pour rediriger. Si vous devez vérifier le token côté serveur (par exemple, pour valider les droits dynamiques), faites-le une seule fois au login et stockez les rôles/permissions localement. Créez deux guards distincts : un AuthGuard qui vérifie simplement que l'utilisateur est connecté, et un RoleGuard qui contrôle l'accès en fonction des droits. Le piège courant est d'ajouter une logique asynchrone dans le guard (appels HTTP) sans gérer les timeouts—le guard peut bloquer indéfiniment l'accès si le serveur ne répond pas.

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivateFn {
  constructor(private auth: AuthService, private router: Router) {}
  
  canActivate: CanActivateFn = (route, state) => {
    if (this.auth.getToken()) {
      return true;
    }
    this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
    return false;
  };
}

@Injectable({ providedIn: 'root' })
export class RoleGuard implements CanActivateFn {
  constructor(private auth: AuthService, private router: Router) {}
  
  canActivate: CanActivateFn = (route, state) => {
    const requiredRole = route.data['role'];
    const userRole = this.auth.getCurrentUserRole();
    
    if (userRole === requiredRole) {
      return true;
    }
    this.router.navigate(['/403']);
    return false;
  };
}

Intégrez le guard dans la configuration de routing : { path: 'admin', component: AdminComponent, canActivate: [AuthGuard, RoleGuard], data: { role: 'admin' } } . Utilisez les guards de fonction (CanActivateFn) plutôt que les classes—c'est plus léger et plus facile à tester.

Gestion de l'expiration et du renouvellement silencieux

Un JWT a une durée de vie limitée (généralement 15 minutes). Attendre que l'utilisateur soit redirigé vers le login après expiration crée une mauvaise UX. Mieux vaut renouveler le token silencieusement avant qu'il n'expire. Implémentez un interceptor qui détecte les réponses 401, appelle le endpoint de refresh, et rejoue la requête originale. Mais attention : si plusieurs requêtes arrivent simultanément après expiration, vous risquez d'appeler refresh plusieurs fois en parallèle. Utilisez un Subject pour centraliser cette logique—une seule requête de refresh en cours, les autres attendent le résultat.

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
  private refreshInProgress = false;
  private refreshSubject = new Subject<string>();
  
  constructor(private auth: AuthService) {}
  
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = this.auth.getToken();
    if (token) {
      req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
    }
    
    return next.handle(req).pipe(
      catchError(error => {
        if (error.status === 401 && !this.refreshInProgress) {
          this.refreshInProgress = true;
          return this.auth.refreshToken().pipe(
            tap(() => {
              this.refreshInProgress = false;
              this.refreshSubject.next(this.auth.getToken()!);
            }),
            switchMap(() => {
              const newToken = this.auth.getToken();
              const clonedReq = req.clone({
                setHeaders: { Authorization: `Bearer ${newToken}` }
              });
              return next.handle(clonedReq);
            }),
            catchError(() => {
              this.refreshInProgress = false;
              return throwError(() => error);
            })
          );
        }
        return throwError(() => error);
      })
    );
  }
}

Pièges courants et anti-patterns

Le piège le plus fréquent est de stocker le token dans localStorage sans vérifier son expiration avant de l'utiliser. Un token expiré mais présent en localStorage sera envoyé au serveur, causant des 401 répétés. Décoder le JWT côté client (sans vérification de signature—c'est juste du base64) et comparer l'expiration ( exp claim) avec Date.now() avant chaque utilisation. Un autre piège : oublier que JWT n'offre aucune protection contre la modification côté client. Le token peut être lu et décodé par n'importe quel script. Ne stockez jamais de données sensibles (mot de passe, numéro de carte) dans le token. Enfin, beaucoup de projets créent un guard asynchrone qui appelle l'API à chaque navigation—cela tue les performances. Préférez une vérification rapide en mémoire, et synchronisez l'état de l'utilisateur une seule fois au démarrage de l'app.

Conclusion opérationnelle

Implémenter JWT + guards robustes n'est pas sorcier, mais demande de la rigueur. Résumé : centralisez votre logique d'authentification dans un service dédié, utilisez des guards synchrones basés sur l'état en mémoire, gérez l'expiration avec un interceptor intelligent, et testez les cas limites (réseau lent, expiration simultanée, multi-onglets). Si vous utilisez httpOnly cookies côté serveur, vous gagnez en sécurité mais perdez en flexibilité cross-domain. Évaluez votre contexte : une app interne peut tolérer localStorage + tokens courts ; une app publique devrait préférer les cookies sécurisés. Dans tous les cas, jamais de tokens en variables globales, jamais de données sensibles dans le JWT, et toujours un endpoint de logout qui invalide le token côté serveur (surtout si vous gérez les sessions).

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️