angularjwtnestjsrefresh-tokenangular16

How to avoid multiple refresh token call, if there are multiple API calls get unauthorized because access token expired


I am trying to implement authentication with JWT access and refresh token, for an archive website. Here during the refresh token call it is somewhat authenticating but not very efficient as shown in the image below, when there is multiple API calls the refresh token also is called multiple times and final one is authenticated and the session is refreshed. I want to avoid and just have one refresh call after that getting authenticated then want it to continue with API calls.

Multiple calls pic

Below is the Code that I tried as of now, regarding the structure I have an authentication interceptor which intercepts the requests adds headers for authentication in backend. Another error interceptor intercepts the Http errors if 401 error occurs it intercepts it and refresh function is called for handle refresh token strategy.

Authentication Interceptor:

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private storage: StorageService, private authService: AuthService) { }

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    const storedTokens = this.authService.getTokens()    
    if (storedTokens.access_token && !request.url.includes('/auth/refresh') ) {
      const cloned = request.clone({
        headers: request.headers.set("Authorization", storedTokens.access_token)
      });
      return next.handle(cloned);
    }
    else if (storedTokens.refresh_token && request.url.includes('/auth/refresh')){
      const cloned = request.clone({
        headers: request.headers.set("Authorization", storedTokens.refresh_token)
      });
      return next.handle(cloned);
    }
    else {
      return next.handle(request);
    }
  }
}

Error Interceptor:

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  constructor(private router: Router, private authService: AuthService) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    return next.handle(request).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          // Access token has expired, attempt to refresh token
          return this.authService.refresh().pipe(
            switchMap(() => {
              const storedTokens = this.authService.getTokens()
              const updatedRequest = request.clone({
              headers: request.headers.set("Authorization", `${storedTokens.access_token}`)
              });
              return next.handle(updatedRequest);
            }),
            catchError((refreshError: any) => {
              // Token refresh failed or refresh token is invalid
              // Redirect user to login page
              this.authService.logout();
              return throwError(refreshError);
            })
          );
        } else if (error.status === 403) {
          // Unauthorized, redirect to login page
          this.authService.logout();
        }
        return throwError(() => new Error(error.message));
      })
    );
  }
}

Solution

  • Use the rxjs share() operator whenever you want to make one http call for potentially multiple method callers/subscribers. Use a singleton service to store the shared observable.

    Http Service:

    refreshTokenObservable?: Observable<unknown>;
    refreshToken(): Observable<unknown> {
        if (this.refreshTokenObservable) {
          return this.refreshTokenObservable;
        }
    
        this.refreshTokenObservable = this.authService.refresh()
          .pipe(share());
    
        return this.refreshTokenObservable;
    }
    

    Error Interceptor:

    @Injectable()
    export class ErrorInterceptor implements HttpInterceptor {
      constructor(private router: Router, private authService: AuthService) { }
    
      intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    
        return next.handle(request).pipe(
          catchError((error: HttpErrorResponse) => {
            if (error.status === 401) {
              // Access token has expired, attempt to refresh token
              return this.httpService.refreshToken().pipe(
                switchMap(() => {
                  const storedTokens = this.authService.getTokens()
                  const updatedRequest = request.clone({
                  headers: request.headers.set("Authorization", `${storedTokens.access_token}`)
                  });
                  return next.handle(updatedRequest);
                }),
                catchError((refreshError: any) => {
                  // Token refresh failed or refresh token is invalid
                  // Redirect user to login page
                  this.authService.logout();
                  return throwError(refreshError);
                })
              );
            } else if (error.status === 403) {
              // Unauthorized, redirect to login page
              this.authService.logout();
            }
            return throwError(() => new Error(error.message));
          })
        );
      }
    }