Search code examples
javascriptangulartypescriptrxjsangular-httpclient

Angular 5 HttpInterceptor refresh token before request


I need to refresh token in HttpInterceptor before the request is made, to do it I check the access token before the request and call refresh if it's expired. Currently, my interceptor looks like this:

@Injectable()
export class TokenInterceptor implements HttpInterceptor {

  private refreshTokenSubject = new BehaviorSubject(null);
  private refreshTokenObservable = this.refreshTokenSubject.asObservable();
  private isRefreshingToken = false;

  constructor(private authService: AuthService) {
  }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const tokenData = AuthService.getCurrentSessionData();

    if (!this.isRefreshingToken) {
      // if no token set - make request as-is
      const tokenSet = tokenData && tokenData.token;
      if (!tokenSet) {
        return next.handle(request);
      }

      // proceed if token not expired
      const tokenExpired = new Date(tokenData.expirationDate) < new Date();
      if (!tokenExpired) {
        return next.handle(this.setAuthHeader(request, tokenData.token));
      }

      // check if we can refresh the token and logout instantly if not
      const tokenRefreshable = tokenData.refreshToken && new Date(tokenData.refreshTokenExpirationDate) > new Date();
      if (!tokenRefreshable) {
        this.authService.logout();
        return Observable.throw('');
      }

      this.isRefreshingToken = true;

      // make all subsequent requests wait for new token
      this.refreshTokenSubject.next(null);

      // make refresh request
      return this.authService.refreshToken()
        .switchMap((res: any) => {
          AuthService.storeSessionData(res, Utils.getLocalStorageItem(STORAGE_KEYS.STAY_LOGGED_IN));
          this.isRefreshingToken = false;

          // let subsequent awaiting proceed
          this.refreshTokenSubject.next(res.access_token);
          return next.handle(this.setAuthHeader(request, res.access_token));
        })
        .catch((err) => {
          this.authService.logout();
          return Observable.throw('');
        })
        .finally(() => {
          this.isRefreshingToken = false;
        });
    } else {
      // if token refreshing in progress - wait for new token
      return this.refreshTokenObservable
        .filter(token => token !== null)
        .take(1)
        .switchMap((token) => {
          return next.handle(this.setAuthHeader(request, token));
        });
    }
  }

  private setAuthHeader(request, token) {
    return request.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`,
      },
    });
  }
}

The problem is that this.authService.refreshToken() never makes the request and subsequent requests never proceed. I guess it's because nothing subscribes to the observable returned from HttpClient, here is the refreshToken method code:

public refreshToken() {
    const tokenData = AuthService.getCurrentSessionData();

    return this.http.post(
      `${environment.apiPath}/auth/refresh`,
      { refresh_token: tokenData.refreshToken },
    );
}

How can I fix this code to make refreshToken request and let other requests proceed after it as intended?


Solution

  •  @Injectable()
        export class RequestInterceptorService implements HttpInterceptor {
    
          @BlockUI() blockUI: NgBlockUI;
          isRefreshingToken = false;
          tokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);
          oldToken = localStorage.getItem('access_token');
          constructor(private authService: AuthService, private localStorageToken: AppLocalStorage,
                      private route: ActivatedRoute, private router: Router) {}
    
          addToken(req: HttpRequest<any>, token: string): HttpRequest<any> {
            return req.clone({ setHeaders: { Authorization: 'Bearer ' + token, 'Access-Control-Allow-Origin': '*' }});
          }
    
          intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpSentEvent | HttpHeaderResponse
            | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> {
            return next.handle(this.addToken(req, this.authService.getAuthToken()))
              .catch(error => {
                if (error instanceof HttpErrorResponse) {
                  switch ((<HttpErrorResponse>error).status) {
                    case 400:
                      return this.handle400Error(error);
                    case 401: this.authService.refresh().subscribe((data) => { // A call has been made at an instant where network get 401 status code,It will prevent the asynchronous way of handling network calls.
                        this.localStorageToken.setRefreshTokens(data); //localstorageToken is used to store all the response, including access token, expiry time and refresh token to get stored in localstorage.
                      }
                    );
                      return this.handle401Error(req, next);
                    // default: this.logoutUser();
                  }
                } else {
                  return Observable.throw(error);
                }
              });
    
          }
    
          handle400Error(error) {
            if (error && error.status === 400 && error.error && error.error.error === 'invalid_grant') {
              // If we get a 400 and the error message is 'invalid_grant', the token is no longer valid so logout.
              // console.log('Bad Error');
              return this.logoutUser();
            }
    
            return Observable.throw(error);
          }
    
          handle401Error(req: HttpRequest<any>, next: HttpHandler) {
            if (!this.isRefreshingToken) {
              this.isRefreshingToken = true;
    
              // Reset here so that the following requests wait until the token
              // comes back from the refreshToken call.
              this.tokenSubject.next(null);
    
              return this.authService.refreshToken()
                .switchMap((newToken: string) => {
                  newToken = localStorage.getItem('access_token');
                  if (newToken) {
                    this.tokenSubject.next(newToken);
                    if (this.oldToken === newToken) {
                      return this.logoutUser();
                    } else {
                      return next.handle(this.addToken(req, newToken));
                    }
                  }
    
                  // If we don't get a new token, we are in trouble so logout.
                  return this.logoutUser();
                })
                .catch(error => {
                  // If there is an exception calling 'refreshToken', bad news so logout.
                  return this.logoutUser();
                })
                .finally(() => {
                  this.isRefreshingToken = false;
                });
            } else {
              return this.tokenSubject
                .filter(token => token != null)
                .take(1)
                .switchMap(token => {
                  return next.handle(this.addToken(req, token));
                });
            }
          }
    
          logoutUser() {
            // Route to the login page (implementation up to you)
            this.router.navigate(['login']).then(() => { this.blockUI.stop(); });
            return Observable.throw('');
          }
        }
    

    Now the AuthService are for getting the refresh token and also for login, This service are called whenever refresh token are needed, I gave 2 sec gap for fetching the refresh token

        export class AuthService {
          private TokenApi = AppSettings.DEVELOPMENT_API;
          private newToken = ' ';
          private current_token: string;
          private refresh_token: string = localStorage.getItem('refresh_token');
    
          constructor(private http: HttpClient, private localStorageToken: AppLocalStorage) {
          }
    
          login(username: string, password: string): Observable<TokenParams> {
            const headers = new HttpHeaders({'content-type': 'application/x-www-form-urlencoded'});
            const loginApi = this.TokenApi + '/oauth/token?username=' + username + '&password=' + password + '&grant_' +
              'type=password....';
            return this.http.post<TokenParams>(loginApi, '', {headers: headers});
          }
    
          refresh(): Observable<TokenParams> {
            this.refresh_token = localStorage.getItem('refresh_token');
            const refreshToken = this.TokenApi + '/oauth/token?refresh_token=' + this.refresh_token + '&grant_' +
              'type=refresh_token...';
            return this.http.post<TokenParams>(refreshToken, '' );
          }
    
          logout() {
            this.localStorageToken.emptyLocalStorage();
          }
    
          getAuthToken() {
            this.current_token = localStorage.getItem('access_token');
            return this.current_token;
          }
    
          refreshToken(): Observable<string> {
           this.newToken = localStorage.getItem('access_token');
            this.current_token = this.newToken;
            return Observable.of(localStorage.getItem('access_token')).delay(2000);
          }
    
        }