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

52 views Asked by At

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));
      })
    );
  }
}
2

There are 2 answers

1
Jineapple On BEST ANSWER

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));
      })
    );
  }
}
2
Afif Alfiano On

I think it's because the token was expired and the component still hit several APIs (not auth API). So, after you get an error 401, the interceptor will hit API for a refresh token, and at the same time, the component hits several APIs on ngOnInit.

For the solution, I think you can try two options:

  • First, create a global state to validate or check the token which means not expired and valid. Then wrap the function of hit API to execute the function only if the token is valid.

      getProductList(): void {
       if (isValidToken) {
         this.serviceName.hitApi().subscribe...;
        }
      }
    
  • Second, Assign the function of hit API on the variable with type subscription and unsubscribe it cancel duplicate HTTP calls on the network, for example:

      getProductList(): void {
       this.$subscription?.unsubscribe()
       this.$subscription = this.serviceName.hitApi().subscribe...;
      }
    

I hope it can help you. Thank you.