AngularAtlased. 22.2

Ch. 17Security & globalisation73 / 75

Authentication: tokens & refresh

An interceptor that attaches the token, refreshes once on 401 and replays the queued requests — plus where you keep the token.

~0 min read Official docs

Most applications use two tokens. A short-lived access token (a few minutes) accompanies every request. A long-lived refresh token lets you obtain a new access token without the user logging in again. The hard part is not attaching a header, but the moment the access token expires while five requests are already in flight.

The page builds, step by step, a functional interceptor that attaches the token, on a 401 makes a single refresh shared by all the waiting requests, and then replays them. You will also see where it is good to keep the tokens and what to do when the refresh fails.

Why it matters
A naively written auth interceptor works perfectly in development, where you make one request at a time. In production, a dashboard that starts 6 requests at once with an expired token produces 6 refreshes, races between tokens and, with “refresh token rotation”, random logouts. The bug is hard to reproduce, but easy to avoid.

The full flow

The access token travels in the Authorization: Bearer … header. When the server answers 401, the application asks the refresh endpoint for a new token. The refresh token is not sent from JavaScript: it sits in an HttpOnly cookie, which the browser sends automatically (withCredentials). Then the original request is repeated with the new token. For the user, everything is invisible.

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

The interceptor: one refresh in flight

Three details make the difference:

  1. The interceptor attaches the token only to the URLs of your own API, so you don’t send it to third parties.
  2. The refresh request is marked with an HttpContextToken (SKIP_AUTH), so the interceptor does not intercept itself endlessly (see HttpClient & interceptors).
  3. The session keeps the current refresh in a field. The first 401 starts it, and all the others subscribe to the same Observable (shareReplay(1)). finalize clears the field at the end.

There is one subtle case: a request can receive a 401 after another one has already finished the refresh. Then the current token differs from the one used, so it simply retries, without another refresh.

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

/** Marked requests get no token and do not trigger a refresh (e.g. 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); // only your API!

  const used = session.token();
  return next(withToken(req, used)).pipe(
    catchError((err: unknown) => {
      if (!(err instanceof HttpErrorResponse) || err.status !== 401) return throwError(() => err);
      // another request already refreshed the token while this one was in flight → we just retry
      if (session.token() && session.token() !== used) return next(withToken(req, session.token()));
      return session.refresh().pipe(switchMap((fresh) => next(withToken(req, fresh))));
    }),
  );
};
LiveFive requests with an expired token (real HttpClient, simulated server)
token: acc-1requests: 0401: 0refreshes: 0succeeded: 0
  1. Press 1, then 2. The log shows the exact order of the requests.
Press “Expire the token”, then send the 5 requests. In the correct variant you see 5 × 401, a single refresh and 5 successful retries. Switch to “naive” and repeat: 5 refreshes, tokens that invalidate each other and requests that fail. Tick “the refresh fails” to see the logout.

The demo has its own HttpClient, created with createEnvironmentInjector([provideHttpClient(withInterceptors([…]))], …). Its interceptors (auth + a simulated “server”, placed last in the chain) do not touch the rest of the application.

The naive variant and why it breaks

If every 401 makes its own refresh, the number of refreshes grows with the number of parallel requests. Many servers rotate the refresh token (each refresh invalidates the previous one), so concurrent refreshes cancel each other out. Some even treat the reuse of an old refresh token as an attack and close the whole session.

// ✗ The naive version: each 401 makes its own refresh
catchError((err) =>
  err.status === 401
    ? http.post('/auth/refresh', null).pipe(switchMap((r) => next(withToken(req, r.accessToken))))
    : throwError(() => err),
);
// 5 parallel requests with an expired token → 5 refreshes.
// With “refresh token rotation”, each refresh invalidates the previous one → some requests fail again,
// and some servers detect the reuse as an attack and close the whole session.

Where to keep the tokens

Any JavaScript code on your page can read localStorage, including a script injected through an XSS (see Security). An HttpOnly cookie cannot be read from JavaScript, but it is sent automatically, so you need SameSite and XSRF protection. The usual compromise: the short-lived access token in memory (a signal), the refresh token in an HttpOnly cookie. The safest option is a BFF (backend-for-frontend): the OAuth tokens stay on the server, and the browser has only a session cookie.

LiveWhere do you keep the token? Trade-offs
If the application has an XSS it cannot be read directly, but injected code can make requests while the page is open
When the page reloads lost → refresh token (cookie) at startup
CSRFnot sent automatically → no CSRF
Several tabseach tab refreshes separately

recommended Good for the short-lived access token, combined with a refresh token in an HttpOnly cookie.

Pick an option and compare what happens with XSS, on reload, with CSRF and with several tabs.

Startup, guards and tabs

When the application starts, the access token in memory is missing. A provideAppInitializer tries a refresh before the first route, so a logged-in user does not see the login page even for a moment. The authentication state is a signal, and the functional guard reads it and redirects with createUrlTree (see Guards & resolvers). Logout must be propagated to all tabs. A BroadcastChannel does this in a few lines (see also Angular Elements).

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])),
    provideAppInitializer(() => {
      // on startup: is there a refresh token in the cookie? we get an access token before the first page
      const session = inject(AuthSession);
      return firstValueFrom(session.refresh()).catch(() => undefined);
    }),
  ],
};

When YES, when NO

Write your own interceptor
  • Your backend issues the tokens (its own JWT) and you only need attaching + refresh.
  • You want full control over the request queue and the error cases.
  • You have a BFF, and the interceptor only adds withCredentials and handles 401 → login.
Use something else
  • OAuth 2 / OpenID Connect with an external provider (Entra ID, Auth0, Keycloak): use a certified OIDC library, which implements PKCE, rotation and the validations correctly. The flow itself, step by step and with real cryptography, is on the page about <a href="/oauth-pkce">OAuth 2.1 and PKCE</a>.
  • Applications with high security requirements: choose a BFF instead of tokens in the browser.
  • Classic cookie sessions, without tokens: you only need XSRF and handling of 401.

Common pitfalls

  • Token sent everywhere: an interceptor without a URL filter attaches Authorization to requests to CDNs or third-party APIs too.
  • The interceptor intercepts the refresh: a 401 on the refresh starts another refresh, and so on. Mark the request with an HttpContextToken.
  • The refresh is not shared: N requests mean N refreshes (see the demo).
  • Retry on any error: only a 401 justifies a refresh. A 403 means “you are not allowed”, and a refresh changes nothing.
  • Logout that doesn’t clean up everything: stores, shareReplay caches, data in localStorage. The next user of the same computer sees them.
  • Decoding the JWT “for security” in the frontend: it is only informative (you display the name). Any permission check is done on the server.

Cheat sheet

TOKENS      short-lived access token (minutes) in MEMORY · long-lived refresh token in an HttpOnly + Secure + SameSite cookie
            or BFF: the browser has only a session cookie, the tokens stay on the server
INTERCEPTOR only for your API's URLs · SKIP_AUTH (HttpContextToken) for login/refresh
401         one refresh in flight (shareReplay + finalize) · retry with the new token
            if the token changed in the meantime → just retry · refresh fails → logout + redirect
STARTUP     provideAppInitializer: try a refresh before the first route
GUARD       CanActivateFn on the isLoggedIn signal → createUrlTree(['/login'], { returnUrl })
TABS        BroadcastChannel / the storage event for synchronous logout
NO          token in localStorage without CSP · a separate refresh on every 401 · an interceptor that also intercepts the refresh

Ch. 17 · 0 of 4 complete

Finished this lesson?

Mark it and it shows up on your map: in the contents and on the home page.

Marks itself as done once you have read the whole lesson.

  1. Security: XSS, sanitisation & CSP
  2. Authentication: tokens & refresh
  3. OAuth 2.1 & OpenID Connect with PKCE
  4. Internationalisation (i18n)
See your whole map

All 75 pages · type to filter

  • 01What's new in Angular 21 & 22All the important changes in v21 and v22, briefly, with a link to the page that explains each one.Start
  • 01Your first app, step by stepA reading list built from an empty component to a finished app — signals, @for, inputs and outputs, a service and a form, one step at a time.Start
  • 01Quick decisions“Where do I put this?”, “Signal or Observable?”, “Shared or feature?” — short answers to everyday questions.Start
  • 01Angular interview questions60 real interview questions for junior, mid and senior, with the answer to give, the follow-up and the red flags. Plus a mock interview.Start
  • 02Standalone & bootstrapHow an app starts without NgModule: bootstrapApplication, app.config.ts, imports in the component.Modern fundamentals
  • 02Data binding & eventsInterpolation, property and attribute bindings, events with $event and key filters, and how [( )] is built — what goes into the DOM and what comes back.Modern fundamentals
  • 02Control flow: @if, @for, @switchThe block syntax that replaces *ngIf/*ngFor/ngSwitch, with track, @empty and exhaustive checks.Modern fundamentals
  • 02@let and the new template expressionsLocal variables with @let, arrow functions, spread, regex, instanceof, comments inside tags.Modern fundamentals
  • 02@defer — lazy loading in templatesLoad heavy components only when needed: triggers, prefetch, placeholder, loading, error.Modern fundamentals
  • 02Class & style bindingsWhy [class.x] and [style.x] replaced ngClass and ngStyle (with automatic migration from v21).Modern fundamentals
  • 02Component styles & encapsulation:host, the three encapsulation modes, theming with CSS variables, and why ::ng-deep is a last resort.Modern fundamentals
  • 02Pipes: when and howPure vs impure pipes, custom pipes, and how you choose between a pipe, computed() and a method.Modern fundamentals
  • 03signal() and computed()The reactive primitives: writable state, derived state, equality, untracked and immutability rules.Signals & reactivity
  • 03effect() — when YES, when NOSide effects synchronised with signals: localStorage, logging, imperative APIs. And their traps.Signals & reactivity
  • 03linkedSignal() — editable derived stateA signal that resets when its source changes, but that you can also set manually.Signals & reactivity
  • 03resource, rxResource, httpResourceAsync data as signals: status, value, error, reload, automatic cancellation. Stable since v22.Signals & reactivity
  • 03debounced() — delayed signalsThe “debounce” version of a signal, without RxJS. Perfect for search.Signals & reactivity
  • 03Lists: search, filter, sort, paginateA chain of computed signals that recalculates only what changed, with the filters kept in the URL so a link reopens the same view.Signals & reactivity
  • 03Observables from zeroWhat an Observable is, subscribe and unsubscribe, the operators you use every week, cold vs hot — and when a signal is enough.Signals & reactivity
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — and when RxJS remains the right choice.Signals & reactivity
  • 04input() and output()Inputs as signals (required, transform, alias) and typed outputs. Data down, events up.Components: communication
  • 04model() — two-way bindingBanana-in-a-box [( )] with a single API. When to use it, and when input + output is better.Components: communication
  • 04viewChild & contentChildQueries as signals: viewChild, viewChildren, contentChild, contentChildren, the read option.Components: communication
  • 04Lifecycle & DestroyRefWhich hooks still matter in the signals era, afterNextRender, DestroyRef and typed ngOnChanges.Components: communication
  • 04Content projectionng-content with select, multiple slots, default content and the trap of conditional projection.Components: communication
  • 04ng-template & ngTemplateOutletTemplates as values: render props in Angular, typed context and customising components.Components: communication
  • 05Smart vs DumbContainer vs presentational: who holds the state, who only displays, and how to split them properly.Component design
  • 05Generic components <T>A correctly typed DataTable<T> or Select<T>, with templates for cells.Component design
  • 05Dynamic componentsNgComponentOutlet, createComponent with inputBinding/outputBinding, and a dashboard built from config.Component design
  • 05Directives & host compositionAttribute directives, the host: {} metadata and the Directive Composition API (hostDirectives).Component design
  • 05Custom form controlsFormValueControl (Signal Forms) vs ControlValueAccessor: a star rating, in both variants.Component design
  • 05Change detectionOnPush is the default from v22, zone.js is gone from v21. What triggers rendering and how not to get burned.Component design
  • 06inject() & @Service()The new @Service() decorator from v22, inject() vs the constructor, and the injection context.Dependency Injection
  • 06Providers & the injector hierarchyRoot vs route vs component, useClass/useValue/useFactory/useExisting, and the optional/self/skipSelf modifiers.Dependency Injection
  • 06InjectionToken & provideX()Typed configuration, provideFeature() functions, makeEnvironmentProviders and initializers.Dependency Injection
  • 06injectAsync() — lazy servicesLoad a heavy service only when you need it, with prefetch on idle.Dependency Injection
  • 07Folder structurecore / shared / features, what goes into each, and what a feature looks like inside.Application architecture
  • 07Shared vs featureWhen to move a component into shared and (especially) when NOT to. A concrete checklist.Application architecture
  • 07Interfaces & modelsInline interface or separate file? DTO vs domain model, type vs interface, where to put them.Application architecture
  • 07State management with signalsSimple signal stores, facades, local vs global state, and when you need NgRx.Application architecture
  • 07Style guide & namingThe current official conventions: file names without suffixes, protected, readonly, member order.Application architecture
  • 07Barrels, aliases & boundariesindex.ts, path aliases, a feature’s public API and dependency rules that can be checked automatically.Application architecture
  • 07Errors: @boundary & ErrorHandlerThe new @boundary block from v22.2, the global ErrorHandler, HTTP errors and errors from resource.Application architecture
  • 08Modern routing & lazy loadingloadComponent, child routes, parameters as inputs and parameter inheritance (the new default in v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate and resolve as functions, with redirects via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — the basicsForms built on signals: form(), [formField], schema-based validation. Stable since v22.Forms
  • 09Signal Forms — advancedCustom and cross-field validation, async/HTTP with debounce, dynamic lists, disabled/hidden, reusable schemas.Forms
  • 09Typed Reactive FormsTyped FormGroup, nonNullable, FormArray — and how they coexist with Signal Forms.Forms
  • 10HttpClient, interceptors & the API layerHttpClient without setup (v21), Fetch by default (v22), functional interceptors and an API layer with DTO mapping.HTTP & data
  • 11Performance — a checklistWhat really matters: OnPush + signals, track, @defer, lazy routes, images, bundles and hydration.Quality & performance
  • 11Testing with VitestVitest is the default runner from v21. TestBed, components with signals, services, HTTP and harnesses.Quality & performance
  • 11DevTools: look inside your appChrome DevTools and Angular DevTools step by step: what each panel shows, with exercises that work on this site.Quality & performance
  • 11Angular AriaHeadless, accessible components that you style however you like: tabs, accordion, menu, listbox...Quality & performance
  • 12SSR, prerender & render modesWhen to render on the server, when at build time and when only in the browser: RenderMode per route, app.routes.server.ts and the real costs.SSR & hydration
  • 12Hydration: event replay & incrementalHow the browser takes over server HTML without re-rendering it: full hydration, event replay, @defer (hydrate on …) and mismatch errors.SSR & hydration
  • 12Server-compatible codewindow doesn’t exist on the server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, the HTTP cache and PendingTasks.SSR & hydration
  • 13Micro frontendsWhen it is worth splitting an app into several deployments and when it is just complexity: Native Federation, Module Federation, iframes, Web Components.Architecture at scale
  • 13Web Components with Angular ElementscreateCustomElement: an Angular component used as an HTML tag anywhere, plus communication between apps through events and BroadcastChannel.Architecture at scale
  • 13Monorepo, libraries & boundariesA workspace with several apps and libraries: ng generate library, Nx, secondary entry points, publishing and dependency rules.Architecture at scale
  • 13Hexagonal architecture (ports & adapters)A domain without Angular, ports as abstract classes and interchangeable adapters: HTTP, localStorage or in-memory, without touching the UI.Architecture at scale
  • 14Signals under the hoodThe reactive graph: producers and consumers, push-pull, why computed is lazy and glitch-free, the diamond problem and when effects run.Advanced reactivity
  • 14Advanced RxJS: higher-order operatorsswitchMap, mergeMap, concatMap and exhaustMap on a live diagram, plus retry with backoff, shareReplay and how to avoid leaks.Advanced reactivity
  • 14NgRx SignalStoreLarge-scale state management on signals: withState, withComputed, withMethods, rxMethod, entities and hooks — and when a simple store is enough.Advanced reactivity
  • 14State machines with signalsNo more impossible states: a typed machine on signals, with transitions, guards and a live state diagram.Advanced reactivity
  • 15WebSockets: real time that survives the networkA typed socket as signals: reconnect with backoff and jitter, heartbeat, an outbox while offline, and SSR safety.Real time & offline
  • 15Optimistic updates & rollbackAn instant UI that stays honest: snapshot and rollback, out-of-order responses, idempotency keys.Real time & offline
  • 15Offline-first: Service Worker & cachingThe Angular Service Worker: caching strategies, update prompts and an outbox for changes made offline.Real time & offline
  • 16Web WorkersHeavy computation without blocking the UI: a real worker in the page, message passing and when it is not worth it.Advanced UI & performance
  • 16Huge lists: virtual scroll100,000 rows without lag: CDK virtual scroll, itemSize, trackBy and the alternatives (pagination, infinite scroll).Advanced UI & performance
  • 16Modern animations & View Transitionsanimate.enter and animate.leave instead of the deprecated @angular/animations, CSS transitions and View Transitions on navigation.Advanced UI & performance
  • 16Memory leaks: finding and fixing themSubscriptions, timers and listeners that outlive their component — how to spot them in DevTools and close them.Advanced UI & performance
  • 17Security: XSS, sanitisation & CSPWhat Angular does automatically against XSS, when bypassSecurityTrust becomes dangerous, plus CSP with nonce, Trusted Types and XSRF.Security & globalisation
  • 17Authentication: tokens & refreshAn interceptor that attaches the token, refreshes once on 401 and replays the queued requests — plus where you keep the token.Security & globalisation
  • 17OAuth 2.1 & OpenID Connect with PKCEThe authorization code flow with PKCE, step by step: verifier and challenge, state and nonce, the ID token, and why a BFF is safer.Security & globalisation
  • 17Internationalisation (i18n)$localize, the i18n attribute, ICU plural/select, a build per language vs runtime translations, and locale-based formatting.Security & globalisation