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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A shared flag plus a BehaviorSubject lets many failing requests wait for one in-flight refresh instead of stampeding the auth server.
- 2Interceptors can retry a failed request by re-issuing it down the same handler chain with a fresh header.
- 3Guarding the refresh URL itself prevents an infinite loop when the refresh call also returns 401.
Related explainers
java
public class SlidingLogRateLimiter { private final int maxRequests; private final long windowMillis;
How a sliding-log rate limiter works
rate-limiting
concurrency
sliding-window
Advanced
8 steps
typescript
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigService } from '@nestjs/config';
How a JWT strategy authenticates in NestJS
authentication
jwt
passport
Intermediate
7 steps
go
package theme import ( "fmt"
Per-tenant HTML templates with Gin's renderer
multi-tenancy
concurrency
html-templates
Advanced
8 steps
python
import time import threading from enum import Enum from functools import wraps
Building a circuit breaker in Python
circuit-breaker
resilience
decorators
Advanced
7 steps
go
package auth import ( "net/http"
Setting and reading secure session cookies in Go
cookies
session-management
security
Intermediate
6 steps
php
<?php namespace App\Http\Middleware;
Idempotent requests with Laravel middleware
idempotency
middleware
caching
Advanced
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/refreshing-auth-tokens-in-an-angular-interceptor-explained-typescript-e577/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.