Angular 4 Interceptor retry requests after token refresh Angular 4 Interceptor retry requests after token refresh javascript javascript

Angular 4 Interceptor retry requests after token refresh


My final solution. Works with parallel requests.

UPDATE: The code updated with Angular 9 / RxJS 6, error handling and fix looping when refreshToken fails

import { HttpRequest, HttpHandler, HttpInterceptor, HTTP_INTERCEPTORS } from "@angular/common/http";import { Injector } from "@angular/core";import { Router } from "@angular/router";import { Subject, Observable, throwError } from "rxjs";import { catchError, switchMap, tap} from "rxjs/operators";import { AuthService } from "./auth.service";export class AuthInterceptor implements HttpInterceptor {    authService;    refreshTokenInProgress = false;    tokenRefreshedSource = new Subject();    tokenRefreshed$ = this.tokenRefreshedSource.asObservable();    constructor(private injector: Injector, private router: Router) {}    addAuthHeader(request) {        const authHeader = this.authService.getAuthorizationHeader();        if (authHeader) {            return request.clone({                setHeaders: {                    "Authorization": authHeader                }            });        }        return request;    }    refreshToken(): Observable<any> {        if (this.refreshTokenInProgress) {            return new Observable(observer => {                this.tokenRefreshed$.subscribe(() => {                    observer.next();                    observer.complete();                });            });        } else {            this.refreshTokenInProgress = true;            return this.authService.refreshToken().pipe(                tap(() => {                    this.refreshTokenInProgress = false;                    this.tokenRefreshedSource.next();                }),                catchError(() => {                    this.refreshTokenInProgress = false;                    this.logout();                }));        }    }    logout() {        this.authService.logout();        this.router.navigate(["login"]);    }    handleResponseError(error, request?, next?) {        // Business error        if (error.status === 400) {            // Show message        }        // Invalid token error        else if (error.status === 401) {            return this.refreshToken().pipe(                switchMap(() => {                    request = this.addAuthHeader(request);                    return next.handle(request);                }),                catchError(e => {                    if (e.status !== 401) {                        return this.handleResponseError(e);                    } else {                        this.logout();                    }                }));        }        // Access denied error        else if (error.status === 403) {            // Show message            // Logout            this.logout();        }        // Server error        else if (error.status === 500) {            // Show message        }        // Maintenance error        else if (error.status === 503) {            // Show message            // Redirect to the maintenance page        }        return throwError(error);    }    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {        this.authService = this.injector.get(AuthService);        // Handle request        request = this.addAuthHeader(request);        // Handle response        return next.handle(request).pipe(catchError(error => {            return this.handleResponseError(error, request, next);        }));    }}export const AuthInterceptorProvider = {    provide: HTTP_INTERCEPTORS,    useClass: AuthInterceptor,    multi: true};


With the latest version of Angular (7.0.0) and rxjs (6.3.3), this is how I created a fully functional Auto Session recovery interceptor ensuring, if concurrent requests fail with 401, then also, it should only hit token refresh API once and pipe the failed requests to the response of that using switchMap and Subject. Below is how my interceptor code looks like. I have omitted the code for my auth service and store service as they are pretty standard service classes.

import {  HttpErrorResponse,  HttpEvent,  HttpHandler,  HttpInterceptor,  HttpRequest} from "@angular/common/http";import { Injectable } from "@angular/core";import { Observable, Subject, throwError } from "rxjs";import { catchError, switchMap } from "rxjs/operators";import { AuthService } from "../auth/auth.service";import { STATUS_CODE } from "../error-code";import { UserSessionStoreService as StoreService } from "../store/user-session-store.service";@Injectable()export class SessionRecoveryInterceptor implements HttpInterceptor {  constructor(    private readonly store: StoreService,    private readonly sessionService: AuthService  ) {}  private _refreshSubject: Subject<any> = new Subject<any>();  private _ifTokenExpired() {    this._refreshSubject.subscribe({      complete: () => {        this._refreshSubject = new Subject<any>();      }    });    if (this._refreshSubject.observers.length === 1) {      this.sessionService.refreshToken().subscribe(this._refreshSubject);    }    return this._refreshSubject;  }  private _checkTokenExpiryErr(error: HttpErrorResponse): boolean {    return (      error.status &&      error.status === STATUS_CODE.UNAUTHORIZED &&      error.error.message === "TokenExpired"    );  }  intercept(    req: HttpRequest<any>,    next: HttpHandler  ): Observable<HttpEvent<any>> {    if (req.url.endsWith("/logout") || req.url.endsWith("/token-refresh")) {      return next.handle(req);    } else {      return next.handle(req).pipe(        catchError((error, caught) => {          if (error instanceof HttpErrorResponse) {            if (this._checkTokenExpiryErr(error)) {              return this._ifTokenExpired().pipe(                switchMap(() => {                  return next.handle(this.updateHeader(req));                })              );            } else {              return throwError(error);            }          }          return caught;        })      );    }  }  updateHeader(req) {    const authToken = this.store.getAccessToken();    req = req.clone({      headers: req.headers.set("Authorization", `Bearer ${authToken}`)    });    return req;  }}

As per @anton-toshik comment, I thought it's a good idea to explain the functioning of this code in a write-up. You can have a read at my article here for the explanation and understanding of this code (how and why it works?). Hope it helps.


I ran into a similar problem as well and I think the collect/retry logic is overly complicated. Instead, we can just use the catch operator to check for the 401, then watch for the token refresh, and rerun the request:

return next.handle(this.applyCredentials(req))  .catch((error, caught) => {    if (!this.isAuthError(error)) {      throw error;    }    return this.auth.refreshToken().first().flatMap((resp) => {      if (!resp) {        throw error;      }      return next.handle(this.applyCredentials(req));    });  }) as any;

...

private isAuthError(error: any): boolean {  return error instanceof HttpErrorResponse && error.status === 401;}