typescript 55 lines · 8 steps

Refreshing auth tokens in an Angular interceptor

An HttpInterceptor that attaches bearer tokens and silently refreshes them on a 401, queuing concurrent requests behind a single refresh.

Explained by highlit
1import { Injectable } from '@angular/core';
2import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
3import { BehaviorSubject, Observable, throwError } from 'rxjs';
4import { catchError, filter, take, switchMap } from 'rxjs/operators';
5import { AuthService } from './auth.service';
6 
7@Injectable()
8export class AuthInterceptor implements HttpInterceptor {
9 private isRefreshing = false;
10 private refreshedToken$ = new BehaviorSubject<string | null>(null);
11 
12 constructor(private auth: AuthService) {}
13 
14 intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
15 const token = this.auth.accessToken;
16 return next.handle(token ? this.withToken(req, token) : req).pipe(
17 catchError(err => {
18 if (err instanceof HttpErrorResponse && err.status === 401 && !req.url.includes('/auth/refresh')) {
19 return this.handle401(req, next);
20 }
21 return throwError(() => err);
22 }),
23 );
24 }
25 
26 private handle401(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
27 if (this.isRefreshing) {
28 return this.refreshedToken$.pipe(
29 filter((t): t is string => t !== null),
30 take(1),
31 switchMap(t => next.handle(this.withToken(req, t))),
32 );
33 }
34 
35 this.isRefreshing = true;
36 this.refreshedToken$.next(null);
37 
38 return this.auth.refresh().pipe(
39 switchMap(({ accessToken }) => {
40 this.isRefreshing = false;
41 this.refreshedToken$.next(accessToken);
42 return next.handle(this.withToken(req, accessToken));
43 }),
44 catchError(err => {
45 this.isRefreshing = false;
46 this.auth.logout();
47 return throwError(() => err);
48 }),
49 );
50 }
51 
52 private withToken(req: HttpRequest<unknown>, token: string): HttpRequest<unknown> {
53 return req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
54 }
55}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1A shared flag plus a BehaviorSubject lets many failing requests wait for one in-flight refresh instead of stampeding the auth server.
  2. 2Interceptors can retry a failed request by re-issuing it down the same handler chain with a fresh header.
  3. 3Guarding the refresh URL itself prevents an infinite loop when the refresh call also returns 401.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Refreshing auth tokens in an Angular interceptor — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code