1
votes

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?

1
"this.authService.refreshToken() never makes the request" The interceptor prevents it from doing that. - a better oliver
@abetteroliver thank you, it was the issue, so stupid) - ilinieja
Did you edit your post to fix the mistake and solve the problem? Where was the problem? Is it because you didn't special case your main interceptor so that it didn't intercept the refresh requests? - Kevin M
@KevinM, no I think I haven't edited the question I worked the issue around by passing noIntercept header in refresh token request and adding the logic that removed this header and bypassed request in the interceptor - ilinieja

1 Answers

1
votes
 @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);
      }

    }