AngularAtlaséd. 22.2

Chap. 17Sécurité et internationalisation73 / 75

Authentification : tokens et refresh

Un interceptor qui ajoute le token, le renouvelle une seule fois sur une 401 et rejoue les requêtes en attente — plus où conserver le token.

La plupart des applications utilisent deux tokens. Un access token de courte durée (quelques minutes) accompagne chaque requête. Un refresh token de longue durée permet d’obtenir un nouvel access token sans que l’utilisateur se reconnecte. Le plus dur n’est pas d’ajouter un en-tête, mais le moment où l’access token expire alors que cinq requêtes sont déjà en cours.

Cette page construit, pas à pas, un interceptor fonctionnel qui ajoute le token, fait sur un 401 un seul refresh partagé par toutes les requêtes en attente, puis les rejoue. Vous verrez aussi où il est judicieux de conserver les tokens et que faire quand le refresh échoue.

Pourquoi c’est important
Un interceptor d’authentification écrit naïvement fonctionne parfaitement en développement, où vous faites une requête à la fois. En production, un tableau de bord qui lance 6 requêtes d’un coup avec un token expiré produit 6 refreshs, des courses entre tokens et, avec la « rotation des refresh tokens », des déconnexions aléatoires. Le bug est difficile à reproduire, mais facile à éviter.

Le flux complet

L’access token voyage dans l’en-tête Authorization: Bearer …. Quand le serveur répond 401, l’application demande un nouveau token à l’endpoint de refresh. Le refresh token n’est pas envoyé depuis JavaScript : il se trouve dans un cookie HttpOnly, que le navigateur envoie automatiquement (withCredentials). Ensuite, la requête d’origine est rejouée avec le nouveau token. Pour l’utilisateur, tout est invisible.

browser                         API                              auth server
   │  GET /orders  (Bearer A1)   │                                    │
   │────────────────────────────▶│  A1 expiré → 401                   │
   │◀────────────────────────────│                                    │
   │  POST /auth/refresh  (cookie HttpOnly avec refresh token)        │
   │─────────────────────────────────────────────────────────────────▶│
   │◀─────────────────────────────────────────── { accessToken: A2 } │
   │  GET /orders  (Bearer A2)   │                                    │
   │────────────────────────────▶│  200                               │

L’Interceptor : un seul refresh en cours

Trois détails font la différence :

  1. L’interceptor ajoute le token uniquement aux URLs de votre propre API, pour ne pas l’envoyer à des tiers.
  2. La requête de refresh est marquée avec un HttpContextToken (SKIP_AUTH), pour que l’interceptor ne s’intercepte pas lui-même à l’infini (voir HttpClient & interceptors).
  3. La session garde le refresh courant dans un champ. Le premier 401 le démarre, et tous les autres s’abonnent au même Observable (shareReplay(1)). finalize vide le champ à la fin.

Il y a un cas subtil : une requête peut recevoir un 401 après qu’une autre a déjà terminé le refresh. Le token courant diffère alors de celui utilisé, donc elle se contente de réessayer, sans autre refresh.

import { HttpContextToken, HttpErrorResponse, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, switchMap, throwError } from 'rxjs';

/** Les requêtes marquées ne reçoivent pas de token et ne déclenchent pas de refresh (p. ex. login, refresh). */
export const SKIP_AUTH = new HttpContextToken<boolean>(() => false);

const withToken = (req: HttpRequest<unknown>, token: string | null) =>
  token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req;

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const session = inject(AuthSession);
  if (req.context.get(SKIP_AUTH) || !req.url.startsWith('/api/')) return next(req); // uniquement votre API !

  const used = session.token();
  return next(withToken(req, used)).pipe(
    catchError((err: unknown) => {
      if (!(err instanceof HttpErrorResponse) || err.status !== 401) return throwError(() => err);
      // une autre requête a déjà renouvelé le token pendant que celle-ci était en cours → on réessaie simplement
      if (session.token() && session.token() !== used) return next(withToken(req, session.token()));
      return session.refresh().pipe(switchMap((fresh) => next(withToken(req, fresh))));
    }),
  );
};
LiveCinq requêtes avec un token expiré (vrai HttpClient, serveur simulé)
token: acc-1requêtes : 0401: 0refreshs : 0réussies : 0
  1. Cliquez sur 1, puis sur 2. Le journal affiche l’ordre exact des requêtes.
Cliquez sur « Faire expirer le token », puis envoyez les 5 requêtes. Dans la variante correcte, vous voyez 5 × 401, un seul refresh et 5 nouvelles tentatives réussies. Passez à « naïf » et recommencez : 5 refreshs, des tokens qui s’invalident mutuellement et des requêtes qui échouent. Cochez « le refresh échoue » pour voir la déconnexion.

La démo a son propre HttpClient, créé avec createEnvironmentInjector([provideHttpClient(withInterceptors([…]))], …). Ses interceptors (auth + un « serveur » simulé, placé en dernier dans la chaîne) n’affectent pas le reste de l’application.

La variante naïve et pourquoi elle casse

Si chaque 401 fait son propre refresh, le nombre de refreshs croît avec le nombre de requêtes parallèles. De nombreux serveurs font tourner le refresh token (chaque refresh invalide le précédent) : les refreshs concurrents s’annulent donc mutuellement. Certains considèrent même la réutilisation d’un ancien refresh token comme une attaque et ferment toute la session.

// ✗ La version naïve : chaque 401 fait son propre refresh
catchError((err) =>
  err.status === 401
    ? http.post('/auth/refresh', null).pipe(switchMap((r) => next(withToken(req, r.accessToken))))
    : throwError(() => err),
);
// 5 requêtes parallèles avec un token expiré → 5 refreshs.
// Avec la « rotation des refresh tokens », chaque refresh invalide le précédent → certaines requêtes échouent à nouveau,
// et certains serveurs détectent la réutilisation comme une attaque et ferment toute la session.

Où conserver les tokens

Tout code JavaScript de votre page peut lire localStorage, y compris un script injecté via une XSS (voir Sécurité). Un cookie HttpOnly ne peut pas être lu depuis JavaScript, mais il est envoyé automatiquement, donc il faut SameSite et une protection XSRF. Le compromis habituel : l’access token de courte durée en mémoire (un signal), le refresh token dans un cookie HttpOnly. L’option la plus sûre est un BFF (backend-for-frontend) : les tokens OAuth restent sur le serveur, et le navigateur n’a qu’un cookie de session.

LiveOù conservez-vous le token ? Compromis
Si l’application a une faille XSS il ne peut pas être lu directement, mais du code injecté peut faire des requêtes tant que la page est ouverte
Au rechargement de la page perdu → refresh token (cookie) au démarrage
CSRFnon envoyé automatiquement → pas de CSRF
Plusieurs ongletschaque onglet fait son propre refresh

recommandé Convient pour l’access token de courte durée, combiné à un refresh token dans un cookie HttpOnly.

Choisissez une option et comparez ce qui se passe avec une XSS, au rechargement, avec CSRF et avec plusieurs onglets.

Démarrage, Guards et onglets

Au démarrage de l’application, l’access token en mémoire est absent. Un provideAppInitializer tente un refresh avant la première route, pour qu’un utilisateur connecté ne voie pas la page de connexion, même un instant. L’état d’authentification est un signal, et le Guard fonctionnel le lit et redirige avec createUrlTree (voir Guards & resolvers). La déconnexion doit être propagée à tous les onglets. Un BroadcastChannel le fait en quelques lignes (voir aussi Angular Elements).

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])),
    provideAppInitializer(() => {
      // au démarrage : y a-t-il un refresh token dans le cookie ? on obtient un access token avant la première page
      const session = inject(AuthSession);
      return firstValueFrom(session.refresh()).catch(() => undefined);
    }),
  ],
};

Quand OUI, quand NON

Écrire votre propre interceptor
  • Votre backend émet les tokens (son propre JWT) et il ne vous faut que l’ajout + le refresh.
  • Vous voulez un contrôle total sur la file de requêtes et les cas d’erreur.
  • Vous avez un BFF, et l’interceptor ajoute seulement withCredentials et gère 401 → connexion.
Utiliser autre chose
  • OAuth 2 / OpenID Connect avec un fournisseur externe (Entra ID, Auth0, Keycloak) : utilisez une bibliothèque OIDC certifiée, qui implémente correctement PKCE, la rotation et les validations. Le flow lui-même, étape par étape et avec une vraie cryptographie, est sur la page <a href="/oauth-pkce">OAuth 2.1 et PKCE</a>.
  • Applications à fortes exigences de sécurité : choisissez un BFF plutôt que des tokens dans le navigateur.
  • Sessions classiques par cookie, sans tokens : il ne vous faut que XSRF et la gestion de 401.

Pièges fréquents

  • Token envoyé partout : un interceptor sans filtre d’URL ajoute Authorization aussi aux requêtes vers des CDN ou des API tierces.
  • L’interceptor intercepte le refresh : un 401 sur le refresh déclenche un autre refresh, et ainsi de suite. Marquez la requête avec un HttpContextToken.
  • Le refresh n’est pas partagé : N requêtes donnent N refreshs (voir la démo).
  • Réessayer sur n’importe quelle erreur : seul un 401 justifie un refresh. Un 403 signifie « vous n’avez pas le droit », et un refresh n’y change rien.
  • Une déconnexion qui ne nettoie pas tout : stores, caches shareReplay, données dans localStorage. Le prochain utilisateur du même ordinateur les voit.
  • Décoder le JWT « par sécurité » dans le frontend : il n’est qu’informatif (vous affichez le nom). Toute vérification de permission se fait sur le serveur.

Aide-mémoire

TOKENS      access token de courte durée (minutes) en MÉMOIRE · refresh token de longue durée dans un cookie HttpOnly + Secure + SameSite
            ou BFF : le navigateur n’a qu’un cookie de session, les tokens restent sur le serveur
INTERCEPTOR uniquement pour les URLs de votre API · SKIP_AUTH (HttpContextToken) pour login/refresh
401         un seul refresh en cours (shareReplay + finalize) · nouvelle tentative avec le nouveau token
            si le token a changé entre-temps → simplement réessayer · refresh échoue → déconnexion + redirection
DÉMARRAGE   provideAppInitializer : tenter un refresh avant la première route
GUARD       CanActivateFn on the isLoggedIn signal → createUrlTree(['/login'], { returnUrl })
ONGLETS     BroadcastChannel / l’événement storage pour une déconnexion synchronisée
À ÉVITER    token dans localStorage sans CSP · un refresh distinct à chaque 401 · un interceptor qui intercepte aussi le refresh

Chap. 17 · 0 sur 4 terminées

Leçon terminée ?

Marquez-la : elle apparaîtra sur votre carte, dans le sommaire et sur la page d’accueil.

Se marque toute seule une fois la leçon lue en entier.

  1. Sécurité : XSS, sanitisation et CSP
  2. Authentification : tokens et refresh
  3. OAuth 2.1 et OpenID Connect avec PKCE
  4. Internationalisation (i18n)
Voir toute votre carte

Les 75 pages · tapez pour filtrer

  • 01Nouveautés d’Angular 21 & 22Tous les changements importants de la v21 et de la v22, en bref, avec un lien vers la page qui explique chacun.Départ
  • 01Votre première application, pas à pasUne liste de lecture construite d’un composant vide jusqu’à une application terminée — signals, @for, inputs et outputs, un service et un formulaire, pas à pas.Départ
  • 01Décisions rapides« Où mettre ça ? », « Signal ou Observable ? », « Shared ou feature ? » — des réponses courtes aux questions de tous les jours.Départ
  • 01Questions d’entretien Angular60 vraies questions d’entretien pour profils junior, confirmé et senior, avec la réponse à donner, la relance et les signaux d’alerte. Plus un entretien blanc.Départ
  • 02Standalone et bootstrapComment une application démarre sans NgModule : bootstrapApplication, app.config.ts, imports dans le composant.Fondamentaux modernes
  • 02Data binding et événementsInterpolation, bindings de propriétés et d’attributs, événements avec $event et filtres de touches, et comment [( )] est construit — ce qui entre dans le DOM et ce qui en revient.Fondamentaux modernes
  • 02Control Flow : @if, @for, @switchLa syntaxe de blocs qui remplace *ngIf/*ngFor/ngSwitch, avec track, @empty et vérifications d’exhaustivité.Fondamentaux modernes
  • 02@let et les nouvelles expressions de templateVariables locales avec @let, fonctions fléchées, spread, regex, instanceof, commentaires dans les balises.Fondamentaux modernes
  • 02@defer — Lazy Loading dans les templatesChargez les composants lourds uniquement quand c’est nécessaire : triggers, prefetch, placeholder, loading, error.Fondamentaux modernes
  • 02Bindings de classe et de stylePourquoi [class.x] et [style.x] ont remplacé ngClass et ngStyle (avec migration automatique depuis la v21).Fondamentaux modernes
  • 02Styles des composants et encapsulation:host, les trois modes d’encapsulation, les thèmes avec des variables CSS, et pourquoi ::ng-deep est un dernier recours.Fondamentaux modernes
  • 02Pipes : quand et commentPipes pures vs impures, pipes personnalisées, et comment choisir entre une pipe, computed() et une méthode.Fondamentaux modernes
  • 03signal() et computed()Les primitives réactives : état modifiable, état dérivé, égalité, untracked et règles d’immutabilité.Signals et réactivité
  • 03effect() — quand OUI, quand NONEffets de bord synchronisés avec les Signals : localStorage, logs, API impératives. Et leurs pièges.Signals et réactivité
  • 03linkedSignal() — état dérivé modifiableUn signal qui se réinitialise quand sa source change, mais que vous pouvez aussi modifier à la main.Signals et réactivité
  • 03resource, rxResource, httpResourceDes données asynchrones sous forme de Signals : status, value, error, reload, annulation automatique. Stable depuis la v22.Signals et réactivité
  • 03debounced() — signals différésLa version « debounce » d’un signal, sans RxJS. Parfait pour la recherche.Signals et réactivité
  • 03Listes : rechercher, filtrer, trier, paginerUne chaîne de signals computed qui ne recalcule que ce qui a changé, avec les filtres dans l’URL pour qu’un lien rouvre la même vue.Signals et réactivité
  • 03Les Observables depuis zéroCe qu’est un Observable, subscribe et unsubscribe, les opérateurs que vous utilisez chaque semaine, froid ou chaud — et quand un signal suffit.Signals et réactivité
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — et quand RxJS reste le bon choix.Signals et réactivité
  • 04input() et output()Les inputs comme Signals (required, transform, alias) et les outputs typés. Les données descendent, les événements remontent.Composants : communication
  • 04model() — binding bidirectionnelLe « banana-in-a-box » [( )] avec une seule API. Quand l’utiliser, et quand input + output est préférable.Composants : communication
  • 04viewChild et contentChildLes queries comme Signals : viewChild, viewChildren, contentChild, contentChildren, l’option read.Composants : communication
  • 04Lifecycle et DestroyRefQuels hooks comptent encore à l’ère des Signals, afterNextRender, DestroyRef et ngOnChanges typé.Composants : communication
  • 04Projection de contenung-content avec select, plusieurs slots, contenu par défaut et le piège de la projection conditionnelle.Composants : communication
  • 04ng-template et ngTemplateOutletLes templates comme valeurs : les render props en Angular, contexte typé et personnalisation des composants.Composants : communication
  • 05Smart vs DumbContainer vs présentation : qui détient l’état, qui se contente d’afficher, et comment bien les séparer.Conception des composants
  • 05Composants génériques <T>Un DataTable<T> ou un Select<T> correctement typé, avec des templates pour les cellules.Conception des composants
  • 05Composants dynamiquesNgComponentOutlet, createComponent avec inputBinding/outputBinding, et un tableau de bord construit à partir d’une configuration.Conception des composants
  • 05Directives et composition d’hôteLes directives d’attribut, les métadonnées host: {} et la Directive Composition API (hostDirectives).Conception des composants
  • 05Contrôles de formulaire personnalisésFormValueControl (Signal Forms) vs ControlValueAccessor : une notation par étoiles, dans les deux variantes.Conception des composants
  • 05Change DetectionOnPush est la valeur par défaut depuis la v22, Zone.js a disparu depuis la v21. Ce qui déclenche le rendu et comment éviter les mauvaises surprises.Conception des composants
  • 06inject() et @Service()Le nouveau décorateur @Service() de la v22, inject() vs le constructeur, et le contexte d’injection.Dependency Injection
  • 06Providers et hiérarchie des injecteursRoot vs route vs composant, useClass/useValue/useFactory/useExisting, et les modificateurs optional/self/skipSelf.Dependency Injection
  • 06InjectionToken et provideX()Configuration typée, fonctions provideFeature(), makeEnvironmentProviders et initializers.Dependency Injection
  • 06injectAsync() — services en Lazy LoadingChargez un service lourd uniquement quand vous en avez besoin, avec prefetch à l’idle.Dependency Injection
  • 07Structure des dossierscore / shared / features, ce que contient chacun, et à quoi ressemble l’intérieur d’une feature.Architecture d’application
  • 07Shared vs featureQuand déplacer un composant dans shared et (surtout) quand NE PAS le faire. Une checklist concrète.Architecture d’application
  • 07Interfaces et modèlesInterface en ligne ou fichier séparé ? DTO vs modèle de domaine, type vs interface, où les ranger.Architecture d’application
  • 07State Management avec les SignalsStores simples à base de Signals, facades, état local vs global, et quand NgRx devient nécessaire.Architecture d’application
  • 07Guide de style et nommageLes conventions officielles actuelles : noms de fichiers sans suffixes, protected, readonly, ordre des membres.Architecture d’application
  • 07Barrels, alias et frontièresindex.ts, alias de chemins, API publique d’une feature et règles de dépendances vérifiables automatiquement.Architecture d’application
  • 07Erreurs : @boundary et ErrorHandlerLe nouveau bloc @boundary de la v22.2, l’ErrorHandler global, les erreurs HTTP et les erreurs de resource.Architecture d’application
  • 08Routing moderne et Lazy LoadingloadComponent, routes enfants, paramètres comme inputs et héritage des paramètres (le nouveau comportement par défaut de la v22).Routing
  • 08Guards et resolverscanActivate, canMatch, canDeactivate et resolve sous forme de fonctions, avec redirections via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — les basesDes formulaires bâtis sur les Signals : form(), [formField], validation par schéma. Stable depuis la v22.Formulaires
  • 09Signal Forms — avancéValidation personnalisée et inter-champs, async/HTTP avec debounce, listes dynamiques, désactivé/masqué, schémas réutilisables.Formulaires
  • 09Reactive Forms typésFormGroup typé, nonNullable, FormArray — et leur cohabitation avec les Signal Forms.Formulaires
  • 10HttpClient, interceptors et couche d’APIHttpClient sans configuration (v21), Fetch par défaut (v22), interceptors fonctionnels et une couche d’API avec mapping des DTO.HTTP et données
  • 11Performance — une checklistCe qui compte vraiment : OnPush + Signals, track, @defer, routes en Lazy Loading, images, bundles et Hydration.Qualité et performance
  • 11Tests avec VitestVitest est le runner par défaut depuis la v21. TestBed, composants avec Signals, services, HTTP et harnesses.Qualité et performance
  • 11DevTools : regarder dans votre applicationChrome DevTools et Angular DevTools pas à pas : ce que montre chaque panneau, avec des exercices qui fonctionnent sur ce site.Qualité et performance
  • 11Angular AriaDes composants headless et accessibles que vous stylez comme vous voulez : tabs, accordion, menu, listbox...Qualité et performance
  • 12SSR, Prerendering et modes de renduQuand rendre sur le serveur, quand au build et quand uniquement dans le navigateur : RenderMode par route, app.routes.server.ts et les coûts réels.SSR et Hydration
  • 12Hydration : event replay et incrémentaleComment le navigateur reprend le HTML du serveur sans le re-rendre : Hydration complète, event replay, @defer (hydrate on …) et erreurs de mismatch.SSR et Hydration
  • 12Code compatible serveurwindow n’existe pas sur le serveur : PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, le cache HTTP et PendingTasks.SSR et Hydration
  • 13Micro frontendsQuand découper une application en plusieurs déploiements vaut le coup, et quand c’est juste de la complexité : Native Federation, Module Federation, iframes, Web Components.Architecture à grande échelle
  • 13Web Components avec Angular ElementscreateCustomElement : un composant Angular utilisé comme balise HTML n’importe où, plus la communication entre applications via des événements et BroadcastChannel.Architecture à grande échelle
  • 13Monorepo, bibliothèques et frontièresUn workspace avec plusieurs applications et bibliothèques : ng generate library, Nx, points d’entrée secondaires, publication et règles de dépendances.Architecture à grande échelle
  • 13Architecture hexagonale (ports et adapters)Un domaine sans Angular, des ports sous forme de classes abstraites et des adapters interchangeables : HTTP, localStorage ou en mémoire, sans toucher à l’UI.Architecture à grande échelle
  • 14Les Signals sous le capotLe graphe réactif : producteurs et consommateurs, push-pull, pourquoi computed est lazy et sans glitch, le problème du diamant et quand les effects s’exécutent.Réactivité avancée
  • 14RxJS avancé : opérateurs d’ordre supérieurswitchMap, mergeMap, concatMap et exhaustMap sur un diagramme interactif, plus retry avec backoff, shareReplay et comment éviter les fuites.Réactivité avancée
  • 14NgRx SignalStoreState Management à grande échelle sur les Signals : withState, withComputed, withMethods, rxMethod, entités et hooks — et quand un store simple suffit.Réactivité avancée
  • 14Machines à états avec les signalsFini les états impossibles : une machine typée sur des signals, avec transitions, gardes et un diagramme d’états en direct.Réactivité avancée
  • 15WebSockets : un temps réel qui résiste au réseauUn socket typé sous forme de signals : reconnexion avec backoff et jitter, heartbeat, file d’attente hors ligne et sécurité SSR.Temps réel et hors ligne
  • 15Mises à jour optimistes et rollbackUne UI instantanée qui reste honnête : snapshot et rollback, réponses dans le désordre, clés d’idempotence.Temps réel et hors ligne
  • 15Offline-first : Service Worker et cacheLe Service Worker d’Angular : stratégies de cache, annonce des nouvelles versions et file d’attente pour les modifications faites hors ligne.Temps réel et hors ligne
  • 16Web WorkersDes calculs lourds sans bloquer l’UI : un vrai worker dans la page, l’échange de messages et quand ça n’en vaut pas la peine.UI avancée et performance
  • 16Listes géantes : virtual scroll100 000 lignes sans latence : le virtual scroll du CDK, itemSize, trackBy et les alternatives (pagination, scroll infini).UI avancée et performance
  • 16Animations modernes et View Transitionsanimate.enter et animate.leave à la place de @angular/animations (déprécié), transitions CSS et View Transitions à la navigation.UI avancée et performance
  • 16Fuites mémoire : les trouver et les corrigerAbonnements, timers et listeners qui survivent à leur composant — comment les repérer dans les DevTools et les fermer.UI avancée et performance
  • 17Sécurité : XSS, sanitisation et CSPCe qu’Angular fait automatiquement contre le XSS, quand bypassSecurityTrust devient dangereux, plus la CSP avec nonce, Trusted Types et XSRF.Sécurité et internationalisation
  • 17Authentification : tokens et refreshUn interceptor qui ajoute le token, le renouvelle une seule fois sur une 401 et rejoue les requêtes en attente — plus où conserver le token.Sécurité et internationalisation
  • 17OAuth 2.1 et OpenID Connect avec PKCELe flux authorization code avec PKCE, pas à pas : verifier et challenge, state et nonce, l’ID token, et pourquoi un BFF est plus sûr.Sécurité et internationalisation
  • 17Internationalisation (i18n)$localize, l’attribut i18n, ICU plural/select, un build par langue vs des traductions à l’exécution, et le formatage selon la locale.Sécurité et internationalisation