AngularAtlased. 22.2

Cap. 17Securitate & globalizare73 / 75

Autentificare: token-uri & refresh

Un interceptor care atașează token-ul, reîmprospătează o singură dată la 401 și reia cererile din coadă — plus unde ții token-ul.

Majoritatea aplicațiilor folosesc două token-uri. Un access token scurt (câteva minute) însoțește fiecare request. Un refresh token lung permite obținerea unui access token nou fără ca utilizatorul să se autentifice din nou. Partea grea nu e să atașezi un header, ci momentul în care access token-ul expiră în timp ce cinci request-uri sunt deja în zbor.

Pagina construiește pas cu pas un interceptor funcțional care atașează token-ul, la 401 face un singur refresh, partajat de toate cererile care așteaptă, și apoi le reia. Vezi și unde e bine să ții token-urile și ce faci când refresh-ul eșuează.

De ce contează
Un interceptor de auth scris naiv merge perfect în dezvoltare, unde faci un request odată. În producție, un dashboard care pornește 6 request-uri simultan cu un token expirat produce 6 refresh-uri, curse între token-uri și, cu „refresh token rotation”, delogări aleatorii. Bug-ul e greu de reprodus, dar ușor de evitat.

Fluxul complet

Access token-ul circulă în header-ul Authorization: Bearer …. Când serverul răspunde 401, aplicația cere un token nou de la endpoint-ul de refresh. Refresh token-ul nu e trimis din JavaScript: stă într-un cookie HttpOnly, trimis automat de browser (withCredentials). Apoi request-ul inițial se repetă cu token-ul nou. Pentru utilizator, totul e invizibil.

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

Interceptorul: un singur refresh în zbor

Trei detalii fac diferența:

  1. Interceptorul atașează token-ul doar pentru URL-urile API-ului tău, ca să nu-l trimiți la terți.
  2. Cererea de refresh e marcată cu un HttpContextToken (SKIP_AUTH), ca interceptorul să nu se intercepteze singur la infinit (vezi HttpClient & interceptori).
  3. Sesiunea ține refresh-ul curent într-un câmp. Primul 401 îl pornește, iar toate celelalte se abonează la același Observable (shareReplay(1)). finalize eliberează câmpul la final.

Mai e un caz fin: un request poate primi 401 după ce altul a terminat deja refresh-ul. Atunci token-ul curent diferă de cel folosit, deci doar reîncerci, fără încă un refresh.

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

/** Cererile marcate nu primesc token și nu declanșează refresh (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); // doar API-ul tău!

  const used = session.token();
  return next(withToken(req, used)).pipe(
    catchError((err: unknown) => {
      if (!(err instanceof HttpErrorResponse) || err.status !== 401) return throwError(() => err);
      // alt request a reîmprospătat deja token-ul cât timp ăsta era în zbor → doar reîncercăm
      if (session.token() && session.token() !== used) return next(withToken(req, session.token()));
      return session.refresh().pipe(switchMap((fresh) => next(withToken(req, fresh))));
    }),
  );
};
LiveCinci request-uri cu token expirat (HttpClient real, server simulat)
token: acc-1request-uri: 0401: 0refresh-uri: 0reușite: 0
  1. Apasă 1, apoi 2. Jurnalul arată ordinea exactă a cererilor.
Apasă „Expiră token-ul”, apoi trimite cele 5 request-uri. În varianta corectă vezi 5 × 401, un singur refresh și 5 reîncercări reușite. Comută pe „naiv” și repetă: 5 refresh-uri, token-uri care se invalidează între ele și request-uri care pică. Bifează „refresh-ul eșuează” ca să vezi logout-ul.

Demo-ul are propriul HttpClient, creat cu createEnvironmentInjector([provideHttpClient(withInterceptors([…]))], …). Interceptorii lui (auth + un „server” simulat, pus ultimul în lanț) nu ating restul aplicației.

Varianta naivă și de ce strică

Dacă fiecare 401 își face propriul refresh, numărul de refresh-uri crește cu numărul de request-uri paralele. Multe servere rotesc refresh token-ul (fiecare refresh îl invalidează pe precedentul), deci refresh-urile concurente se anulează între ele. Unele detectează chiar „refolosirea” unui refresh token vechi ca atac și închid toată sesiunea.

// ✗ Varianta naivă: fiecare 401 își face propriul refresh
catchError((err) =>
  err.status === 401
    ? http.post('/auth/refresh', null).pipe(switchMap((r) => next(withToken(req, r.accessToken))))
    : throwError(() => err),
);
// 5 request-uri paralele cu token expirat → 5 refresh-uri.
// Cu „refresh token rotation”, fiecare refresh îl invalidează pe precedentul → unele request-uri pică din nou,
// iar unele servere detectează refolosirea și închid toată sesiunea.

Unde ții token-urile

Orice cod JavaScript din pagina ta poate citi localStorage, deci și un script injectat printr-un XSS (vezi Securitate). Un cookie HttpOnly nu poate fi citit din JavaScript, dar e trimis automat, așa că ai nevoie de SameSite și protecție XSRF. Compromisul obișnuit: access token-ul scurt în memorie (un semnal), refresh token-ul în cookie HttpOnly. Varianta cea mai sigură e un BFF (backend-for-frontend): token-urile OAuth rămân pe server, iar browserul are doar un cookie de sesiune.

LiveUnde ții token-ul? Compromisuri
Dacă aplicația are un XSS nu poate fi citit direct, dar codul injectat poate face request-uri cât e pagina deschisă
La reîncărcarea paginii se pierde → refresh token (cookie) la pornire
CSRFnu e trimis automat → fără CSRF
Mai multe tab-urifiecare tab face refresh separat

recomandat Bun pentru access token-ul scurt, combinat cu refresh token în cookie HttpOnly.

Alege o variantă și compară ce se întâmplă la XSS, la reîncărcare, la CSRF și cu mai multe tab-uri.

Pornire, guard-uri și tab-uri

La pornirea aplicației, access token-ul din memorie lipsește. Un provideAppInitializer încearcă un refresh înainte de prima rută, așa că utilizatorul autentificat nu vede pagina de login nici măcar o clipă. Starea de autentificare e un semnal, iar guard-ul funcțional îl citește și redirecționează cu createUrlTree (vezi Guards & resolvers). Logout-ul trebuie propagat în toate tab-urile. Un BroadcastChannel face asta cu câteva linii (vezi și Angular Elements).

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])),
    provideAppInitializer(() => {
      // la pornire: există un refresh token în cookie? obținem un access token înainte de prima pagină
      const session = inject(AuthSession);
      return firstValueFrom(session.refresh()).catch(() => undefined);
    }),
  ],
};

Când DA, când NU

Scrie interceptorul tău
  • Backend-ul tău emite token-urile (JWT propriu) și ai nevoie doar de atașare + refresh.
  • Vrei control complet asupra cozii de request-uri și a cazurilor de eroare.
  • Ai un BFF și interceptorul doar adaugă withCredentials și tratează 401 → login.
Folosește altceva
  • OAuth 2 / OpenID Connect cu un provider extern (Entra ID, Auth0, Keycloak): folosește o bibliotecă OIDC certificată, care implementează PKCE, rotația și validările corect. Flow-ul în sine, pas cu pas și cu criptografie reală, e pe pagina despre <a href="/oauth-pkce">OAuth 2.1 și PKCE</a>.
  • Aplicații cu cerințe mari de securitate: alege BFF în loc de token-uri în browser.
  • Sesiuni clasice cu cookie, fără token-uri: îți trebuie doar XSRF și tratarea lui 401.

Capcane frecvente

  • Token trimis oriunde: un interceptor fără filtru pe URL atașează Authorization și la request-uri către CDN-uri sau API-uri terțe.
  • Interceptorul interceptează refresh-ul: un 401 la refresh pornește alt refresh, și tot așa. Marchează cererea cu HttpContextToken.
  • Refresh-ul nu e partajat: N request-uri înseamnă N refresh-uri (vezi demo-ul).
  • Retry pe orice eroare: doar 401 justifică un refresh. 403 înseamnă „nu ai voie”, iar un refresh nu schimbă nimic.
  • Logout care nu curăță tot: store-uri, cache-uri shareReplay, date în localStorage. Utilizatorul următor de pe același calculator le vede.
  • Decodarea JWT-ului „pentru securitate” în frontend: e doar informativă (afișezi numele). Orice verificare de drepturi se face pe server.

Fișă rapidă

TOKEN-URI   access token scurt (minute) în MEMORIE · refresh token lung în cookie HttpOnly + Secure + SameSite
            sau BFF: browserul are doar un cookie de sesiune, token-urile stau pe server
INTERCEPTOR doar pentru URL-urile API-ului tău · SKIP_AUTH (HttpContextToken) pentru login/refresh
401         un singur refresh în zbor (shareReplay + finalize) · retry cu token-ul nou
            dacă token-ul s-a schimbat între timp → doar retry · refresh eșuat → logout + redirect
PORNIRE     provideAppInitializer: încearcă un refresh înainte de prima rută
GUARD       CanActivateFn pe semnalul isLoggedIn → createUrlTree(['/login'], { returnUrl })
TAB-URI     BroadcastChannel / evenimentul storage pentru logout sincron
NU          token în localStorage fără CSP · refresh pe fiecare 401 separat · interceptor care intercepte și refresh-ul

Cap. 17 · 0 din 4 terminate

Ai terminat lecția?

Marcheaz-o și apare pe harta ta: în cuprins și pe prima pagină.

Se marchează singură după ce citești toată lecția.

  1. Securitate: XSS, sanitizare & CSP
  2. Autentificare: token-uri & refresh
  3. OAuth 2.1 și OpenID Connect cu PKCE
  4. Internaționalizare (i18n)
Vezi toată harta ta

Toate cele 75 de pagini · scrie ca să filtrezi

  • 01Ce e nou în Angular 21 & 22Toate schimbările importante din v21 și v22, pe scurt, cu link către pagina care le explică.Start
  • 01Prima ta aplicație, pas cu pasO listă de lecturi construită de la o componentă goală la o aplicație terminată — signals, @for, input-uri și output-uri, un serviciu și un formular, pas cu pas.Start
  • 01Decizii rapide„Unde pun asta?”, „Signal sau Observable?”, „Shared sau feature?” — răspunsuri scurte la întrebările de zi cu zi.Start
  • 01Întrebări de interviu Angular60 de întrebări reale de interviu pentru junior, mid și senior, cu răspunsul de dat, întrebarea care urmează și greșelile de evitat. Plus o simulare de interviu.Start
  • 02Standalone & bootstrapCum pornește o aplicație fără NgModule: bootstrapApplication, app.config.ts, imports în componentă.Fundamente moderne
  • 02Data binding și evenimenteInterpolare, binding pe proprietăți și pe atribute, evenimente cu $event și filtre de taste și cum e construit [( )] — ce intră în DOM și ce se întoarce.Fundamente moderne
  • 02Control flow: @if, @for, @switchSintaxa de blocuri care înlocuiește *ngIf/*ngFor/ngSwitch, cu track, @empty și verificare exhaustivă.Fundamente moderne
  • 02@let și expresiile noi din templateVariabile locale cu @let, arrow functions, spread, regex, instanceof, comentarii în tag-uri.Fundamente moderne
  • 02@defer — lazy loading în templateÎncarci componente grele doar când e nevoie: triggere, prefetch, placeholder, loading, error.Fundamente moderne
  • 02Class & style bindingsDe ce [class.x] și [style.x] au înlocuit ngClass și ngStyle (cu migrare automată din v21).Fundamente moderne
  • 02Stilurile componentelor și încapsularea:host, cele trei moduri de încapsulare, teme cu variabile CSS și de ce ::ng-deep e ultima soluție.Fundamente moderne
  • 02Pipes: când și cumPipe pur vs impur, pipe custom și cum alegi între pipe, computed() și o metodă.Fundamente moderne
  • 03signal() și computed()Primitivele reactive: stare scriibilă, stare derivată, egalitate, untracked și reguli de imutabilitate.Signals & reactivitate
  • 03effect() — când DA, când NUEfecte secundare sincronizate cu semnale: localStorage, logging, API-uri imperative. Și capcanele lor.Signals & reactivitate
  • 03linkedSignal() — stare derivată editabilăUn semnal care se resetează când se schimbă sursa, dar pe care îl poți și seta manual.Signals & reactivitate
  • 03resource, rxResource, httpResourceDate asincrone ca semnale: status, value, error, reload, anulare automată. Stabile din v22.Signals & reactivitate
  • 03debounced() — semnale cu întârziereVarianta „debounce” a unui semnal, fără RxJS. Perfect pentru căutare.Signals & reactivitate
  • 03Liste: căutare, filtrare, sortare, paginareUn lanț de computed-uri care recalculează doar ce s-a schimbat, cu filtrele ținute în URL, ca un link să redeschidă aceeași vedere.Signals & reactivitate
  • 03Observables de la zeroCe e un Observable, subscribe și unsubscribe, operatorii pe care îi folosești în fiecare săptămână, cold vs hot — și când ajunge un signal.Signals & reactivitate
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — și când RxJS rămâne alegerea bună.Signals & reactivitate
  • 04input() și output()Inputuri ca semnale (required, transform, alias) și outputuri tipizate. Data down, events up.Componente: comunicare
  • 04model() — two-way bindingBanana-in-a-box [( )] cu un singur API. Când îl folosești și când e mai bine input + output.Componente: comunicare
  • 04viewChild & contentChildInterogări ca semnale: viewChild, viewChildren, contentChild, contentChildren, opțiunea read.Componente: comunicare
  • 04Lifecycle & DestroyRefCe hook-uri mai contează în era signals, afterNextRender, DestroyRef și ngOnChanges tipizat.Componente: comunicare
  • 04Content projectionng-content cu select, sloturi multiple, conținut implicit și capcana proiecției condiționale.Componente: comunicare
  • 04ng-template & ngTemplateOutletTemplate-uri ca valori: render props în Angular, context tipizat și personalizare de componente.Componente: comunicare
  • 05Smart vs DumbContainer vs presentational: cine ține starea, cine doar afișează, și cum le împarți corect.Design de componente
  • 05Componente generice <T>Un DataTable<T> sau Select<T> tipizat corect, cu template-uri pentru celule.Design de componente
  • 05Componente dinamiceNgComponentOutlet, createComponent cu inputBinding/outputBinding și un dashboard construit din config.Design de componente
  • 05Directive & host compositionDirective atribut, metadata host: {} și Directive Composition API (hostDirectives).Design de componente
  • 05Controale custom de formularFormValueControl (Signal Forms) vs ControlValueAccessor: un rating cu stele, în ambele variante.Design de componente
  • 05Change detectionOnPush e implicit din v22, zone.js lipsește din v21. Ce declanșează randarea și cum să nu te arzi.Design de componente
  • 06inject() & @Service()Noul decorator @Service() din v22, inject() vs constructor și contextul de injecție.Dependency Injection
  • 06Providers & ierarhia de injectoriRoot vs rută vs componentă, useClass/useValue/useFactory/useExisting și modificatorii optional/self/skipSelf.Dependency Injection
  • 06InjectionToken & provideX()Configurare tipizată, funcții provideFeature(), makeEnvironmentProviders și inițializatori.Dependency Injection
  • 06injectAsync() — servicii lazyÎncarci un serviciu greu abia când ai nevoie de el, cu prefetch la idle.Dependency Injection
  • 07Structura de folderecore / shared / features, ce intră în fiecare și cum arată un feature pe dinăuntru.Arhitectura aplicației
  • 07Shared vs featureCând muți o componentă în shared și (mai ales) când NU. Checklist concret.Arhitectura aplicației
  • 07Interfețe & modeleInterfață inline sau fișier separat? DTO vs model de domeniu, type vs interface, unde le pui.Arhitectura aplicației
  • 07State management cu signalsStore-uri simple pe semnale, facade, stare locală vs globală și când îți trebuie NgRx.Arhitectura aplicației
  • 07Style guide & namingConvențiile oficiale actuale: nume de fișiere fără sufixe, protected, readonly, ordinea membrilor.Arhitectura aplicației
  • 07Barrels, aliasuri & granițeindex.ts, path aliases, API public pe feature și reguli de dependență care se pot verifica automat.Arhitectura aplicației
  • 07Erori: @boundary & ErrorHandlerNoul bloc @boundary din v22.2, ErrorHandler global, erori HTTP și erori din resource.Arhitectura aplicației
  • 08Routing modern & lazy loadingloadComponent, rute copil, parametri ca input-uri și moștenirea parametrilor (default nou în v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate și resolve ca funcții, cu redirect prin UrlTree / RedirectCommand.Routing
  • 09Signal Forms — bazeleFormulare construite pe semnale: form(), [formField], validare prin schemă. Stabile din v22.Formulare
  • 09Signal Forms — avansatValidare custom și cross-field, async/HTTP cu debounce, liste dinamice, disabled/hidden, scheme reutilizabile.Formulare
  • 09Reactive Forms tipizateFormGroup tipizat, nonNullable, FormArray — și cum conviețuiesc cu Signal Forms.Formulare
  • 10HttpClient, interceptors & API layerHttpClient fără setup (v21), Fetch implicit (v22), interceptori funcționali și stratul de API cu mapare DTO.HTTP & date
  • 11Performanță — checklistCe chiar contează: OnPush + signals, track, @defer, lazy routes, imagini, bundle și hidratare.Calitate & performanță
  • 11Testare cu VitestVitest e runner-ul implicit din v21. TestBed, componente cu semnale, servicii, HTTP și harness-uri.Calitate & performanță
  • 11DevTools: cum te uiți în aplicațieChrome DevTools și Angular DevTools pas cu pas: ce arată fiecare panou, cu exerciții care merg chiar pe site-ul ăsta.Calitate & performanță
  • 11Angular AriaComponente headless, accesibile, pe care le stilizezi cum vrei: tabs, accordion, menu, listbox...Calitate & performanță
  • 12SSR, prerender & render modesCând randezi pe server, când la build și când doar în browser: RenderMode pe rută, app.routes.server.ts și costurile reale.SSR & hidratare
  • 12Hidratare: event replay & incrementalCum preia browserul HTML-ul de pe server fără să-l refacă: hidratare completă, event replay, @defer (hydrate on …) și erorile de nepotrivire.SSR & hidratare
  • 12Cod compatibil cu serverulwindow nu există pe server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, cache-ul HTTP și PendingTasks.SSR & hidratare
  • 13Micro frontendsCând merită să spargi o aplicație în mai multe deploy-uri și când e doar complexitate: Native Federation, Module Federation, iframes, Web Components.Arhitectură la scară
  • 13Web Components cu Angular ElementscreateCustomElement: o componentă Angular folosită ca tag HTML oriunde, plus comunicarea între aplicații prin evenimente și BroadcastChannel.Arhitectură la scară
  • 13Monorepo, librării & granițeUn workspace cu mai multe aplicații și librării: ng generate library, Nx, entry points secundare, publicare și reguli de dependență.Arhitectură la scară
  • 13Arhitectură hexagonală (ports & adapters)Domeniul fără Angular, porturi ca clase abstracte și adaptoare interschimbabile: HTTP, localStorage sau in-memory, fără să atingi UI-ul.Arhitectură la scară
  • 14Semnalele pe dinăuntruGraful reactiv: producători și consumatori, push-pull, de ce computed e lazy și glitch-free, problema diamantului și când rulează efectele.Reactivitate avansată
  • 14RxJS avansat: operatori de ordin superiorswitchMap, mergeMap, concatMap și exhaustMap pe o diagramă live, plus retry cu backoff, shareReplay și cum eviți scurgerile.Reactivitate avansată
  • 14NgRx SignalStoreState management la scară pe semnale: withState, withComputed, withMethods, rxMethod, entități și hooks — și când un store simplu e suficient.Reactivitate avansată
  • 14Mașini de stări cu signalsFără stări imposibile: o mașină tipizată pe signals, cu tranziții, guards și diagrama stărilor, live.Reactivitate avansată
  • 15WebSockets: timp real care rezistă rețeleiUn socket tipizat ca signals: reconectare cu backoff și jitter, heartbeat, coadă de mesaje cât ești offline și siguranță la SSR.Timp real și offline
  • 15Actualizări optimiste și rollbackInterfață instantanee, dar onestă: snapshot și rollback, răspunsuri sosite în altă ordine, chei de idempotență.Timp real și offline
  • 15Offline-first: Service Worker și cacheService Worker-ul Angular: strategii de cache, anunțarea versiunilor noi și o coadă pentru modificările făcute offline.Timp real și offline
  • 16Web WorkersCalcule grele fără să blochezi interfața: un worker real în pagină, comunicare prin mesaje și când nu merită.UI & performanță avansată
  • 16Liste uriașe: virtual scroll100.000 de rânduri fără lag: CDK virtual scroll, itemSize, trackBy și alternativele (paginare, infinite scroll).UI & performanță avansată
  • 16Animații moderne & View Transitionsanimate.enter și animate.leave în locul @angular/animations (depreciat), tranziții CSS și View Transitions la navigare.UI & performanță avansată
  • 16Memory leaks: cum le găsești și le repariSubscripții, timere și listeneri care trăiesc mai mult decât componenta lor — cum îi vezi în DevTools și cum îi închizi.UI & performanță avansată
  • 17Securitate: XSS, sanitizare & CSPCe face Angular automat împotriva XSS, când devine periculos bypassSecurityTrust, plus CSP cu nonce, Trusted Types și XSRF.Securitate & globalizare
  • 17Autentificare: token-uri & refreshUn interceptor care atașează token-ul, reîmprospătează o singură dată la 401 și reia cererile din coadă — plus unde ții token-ul.Securitate & globalizare
  • 17OAuth 2.1 și OpenID Connect cu PKCEFluxul authorization code cu PKCE, pas cu pas: verifier și challenge, state și nonce, ID token-ul și de ce un BFF e mai sigur.Securitate & globalizare
  • 17Internaționalizare (i18n)$localize, atributul i18n, ICU plural/select, build per limbă vs. traduceri la runtime și formatarea după locale.Securitate & globalizare