2

I have implemented refreshtoken logic using interceptor by referring this link Reference Link. It is working fine as expected. But in this case I am facing one critical issue that when I call more than one api parallelly and all api returns 401 then refreshtoken call more then one time depending upon how fast this.isRefreshing variable value set to true. Now my question is that I want to call refreshtoken api only for one time and all other pending api (which were already returned 401) must be call. How to achieve this?

Screenshot of calls

There are 3 api calls. All returns 401 so all will trying call refreshtoken. Only one refreshtoken api should be call and all other red coloured api should be served one by one or parallelly.

Following is my interceptor.ts.

    import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Injectable, Injector } from "@angular/core";
import { BehaviorSubject, Observable, throwError } from "rxjs";
import { catchError, filter, switchMap, take } from "rxjs/operators";
import { AuthenticationService, CommonService } from "../services";
import { TokenService } from "../services/token.service";

/**
 * Link Refere : https://stackoverflow.com/questions/57637923/angular-8-intercept-call-to-refresh-token
 */
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    private isRefreshing = false;
    private tokenService;
    private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
    /* No Direct Service inject on Constructor
       Using Like: 
       const authService = this.injector.get(AuthenticationService);
    */
    constructor(private injector: Injector) {
        this.tokenService = this.injector.get(TokenService);
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        //const token = localStorage.getItem('accessToken');

        // const token = this.tokenService.accessToken;

        // if (token) {
        //     req = this.addTokenHeader(req, token);
        // }

        req = req.clone({
            withCredentials: true
        });

        return next.handle(req).pipe(catchError(error => {
            // if (error instanceof HttpErrorResponse && !req.url.includes('auth/signin') && error.status === 401) {
            //     return this.handle401Error(req, next);
            // }

            if (error instanceof HttpErrorResponse && error.status === 401) {
                return this.handle401Error(req, next);
            }

            return throwError(error);
        }));
    }

    private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
        console.log("401 error request:", request);
        const authService = this.injector.get(AuthenticationService);
        if (!this.isRefreshing) {
            this.isRefreshing = true;
            this.refreshTokenSubject.next(null);

            return authService.refreshToken().pipe(
                switchMap((token: any) => {
                    this.isRefreshing = false;
                    this.tokenService.accessToken = token.accessToken;
                    this.refreshTokenSubject.next(token.accessToken);
                    return next.handle(request);
                    //return next.handle(this.addTokenHeader(request, token.accessToken));
                }),
                catchError((err) => {
                    this.isRefreshing = false;
                    authService.directRedirectToLogin();

                    // authService.logout().subscribe(data => {
                    //     authService.redirectToLogin();
                    // });
                    return throwError(err);
                })
            );
        }

        return this.refreshTokenSubject.pipe(
            filter(token => token !== null),
            take(1),
            switchMap((token) => next.handle(request))
            //switchMap((token) => next.handle(this.addTokenHeader(request, token)))
        );
    }

    private addTokenHeader(request: HttpRequest<any>, token: string) {
        const setHeaders = {
            Authorization: `Bearer ${token}`,
            'Content-Type': 'application/json'
        }

        if (request.body instanceof FormData) {
            delete setHeaders["Content-Type"];
        }

        return request.clone({
            setHeaders: setHeaders
        });
    }
}
Sumit patel
  • 3,807
  • 9
  • 34
  • 61

2 Answers2

0

Update 02.12.2022: Here is a good example! GitHub

It uses a service which will wait for the refresh token (or a error by refreshing it)! Try this!


Here is my interceptor for this case:

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse,
} from '@angular/common/http';
import {
  BehaviorSubject,
  catchError,
  filter,
  Observable,
  switchMap,
  take,
  throwError,
} from 'rxjs';
import { AuthService } from '../services/auth.service';

@Injectable()
export class BearerInterceptor implements HttpInterceptor {
  private isRefreshing = false;
  private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(
    null
  );

  constructor(private authService: AuthService) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<Object>> {
    let authReq = req;
    const token = this.authService.getToken();
    if (token != null) {
      authReq = this.addTokenHeader(req, token);
    }

    return next.handle(authReq).pipe(
      catchError((error) => {
        if (
          error instanceof HttpErrorResponse &&
          !authReq.url.includes('auth/signin') &&
          error.status === 401
        ) {
          if (authReq.url.includes('auth/token'))
            return throwError(() => new Error(error.error));

          return this.handle401Error(authReq, next);
        }

        return throwError(() => new Error(error));
      })
    );
  }

  private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
    if (!this.isRefreshing) {
      this.isRefreshing = true;
      this.refreshTokenSubject.next(null);

      const token = this.authService.getRefreshToken();

      if (token)
        return this.authService.refreshToken(token).pipe(
          switchMap((token: any) => {
            this.isRefreshing = false;
            this.authService.saveTokens(token.token, token.refreshToken);
            this.refreshTokenSubject.next(token.accessToken);

            return next.handle(this.addTokenHeader(request, token.token));
          }),
          catchError((err) => {
            this.isRefreshing = false;

            this.authService.signOut();
            return throwError(() => new Error(err));
          })
        );
    }

    return this.refreshTokenSubject.pipe(
      filter((token) => token !== null),
      take(1),
      switchMap((token) => next.handle(this.addTokenHeader(request, token)))
    );
  }

  private addTokenHeader(request: HttpRequest<any>, token: string) {
    /* for Spring Boot back-end */
    // return request.clone({ headers: request.headers.set(TOKEN_HEADER_KEY, 'Bearer ' + token) });

    /* for Node.js Express back-end */
    return request.clone({
      headers: request.headers.set('Authorization', 'Bearer ' + token),
    });
  }
}

I think the important part is to check from where the error comes:

if (error instanceof HttpErrorResponse &&
    !authReq.url.includes('auth/signin') &&
    error.status === 401) {
        if (authReq.url.includes('auth/token'))
            return throwError(() => new Error(error.error));

          return this.handle401Error(authReq, next);
        }

So include the url auth/token were the error come from token. Otherwise hold all and wait for the refresh token.

Have fun, Florian

Flo
  • 2,232
  • 2
  • 11
  • 18
0

Try this, I was also faced same issue but now working fine by using this.

import { Injectable } from '@angular/core';
    import {
      HttpEvent,
      HttpHandler,
      HttpInterceptor,
      HttpRequest
    } from '@angular/common/http';
    import { throwError, Observable, BehaviorSubject } from 'rxjs';
    import { catchError, concatMap, filter, finalize, take } from 'rxjs/operators';
    
    import { environment } from '@app/env';
    import { AuthService, RefreshTokenResult } from '../auth.service';
    
    @Injectable()
    export class AuthIntercepter implements HttpInterceptor {
      isRefreshingToken = false;
    
      tokenRefreshed$ = new BehaviorSubject<boolean>(false);
    
      constructor(private authService: AuthService) {}
    
      addToken(req: HttpRequest<any>): HttpRequest<any> {
        const token = this.authService.token;
        return token
          ? req.clone({ setHeaders: { Authorization: 'Bearer ' + token } })
          : req;
      }
    
      intercept(
        req: HttpRequest<any>,
        next: HttpHandler
      ): Observable<HttpEvent<any>> {
        return next.handle(this.addToken(req)).pipe(
          catchError((err) => {
            if (err.status === 401) {
              return this.handle401Error(req, next);
            }
    
            return throwError(err);
          })
        );
      }
    
      private handle401Error(
        req: HttpRequest<any>,
        next: HttpHandler
      ): Observable<any> {
        if (this.isRefreshingToken) {
          return this.tokenRefreshed$.pipe(
            filter(Boolean),
            take(1),
            concatMap(() => next.handle(this.addToken(req)))
          );
        }
    
        this.isRefreshingToken = true;
    
        // Reset here so that the following requests wait until the token
        // comes back from the refreshToken call.
        this.tokenRefreshed$.next(false);
    
        return this.authService.refreshToken().pipe(
          concatMap((res: RefreshTokenResult) => {
            if (!environment.production) {
              console.info('Token was successfully refreshed'); // tslint:disable-line
            }
    
            this.tokenRefreshed$.next(true);
            return next.handle(this.addToken(req));
          }),
          catchError((err) => {
            this.authService.logout();
            return throwError(err);
          }),
          finalize(() => {
            this.isRefreshingToken = false;
          })
        );
      }
    }
Alex
  • 11
  • 2