angularjwtangular-http-interceptors

Trying to repeat a http request after refresh token with a interceptor in angular 7


I'm trying to automate the refresh token requests upon receiving an error 401 with angular 7.

Between that I do not find much documentation of how to do it with angular 7 and that I do not have previous knowledge of angular or rxjs I am becoming a little crazy

I think it's almost completed, but for some reason the second next.handle(newReq) dont send the request (in google chrome network debugger only apears first request)

i'm gettin the response of refresh and making processLoginResponse(res) correctly

you can see here my interceptor

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

let newReq = req.clone();

return next.handle(req).pipe(
  catchError(error => {
    if (error.status == 401) {
      this._authenticationService.refresh().subscribe(
        res => {
          this._authenticationService.processLoginResponse(res);
          newReq.headers.set("Authorization", "Bearer " + this._authenticationService.authResponse.token)
          return next.handle(newReq)
        },
        error => {
          this._authenticationService.logOut();
        });
    }
    throw error;
  })
);

Solution

  • You have to distingiush among all the requests. For example you don't want to intercept your login request and also not the refresh token request. SwitchMap is your best friend because you need to cancel some calls to wait for your token is getting refreshed.

    So what you do is check first for error responses with status 401 (unauthorized):

    return next.handle(this.addToken(req, this.userService.getAccessToken()))
                .pipe(catchError(err => {
                    if (err instanceof HttpErrorResponse) {
                        // token is expired refresh and try again
                        if (err.status === 401) {
                            return this.handleUnauthorized(req, next);
                        }
    
                        // default error handler
                        return this.handleError(err);
    
                    } else {
                        return observableThrowError(err);
                    }
                }));
    

    In your handleUnauthorized function you have to refresh your token and also skip all further requests in the meantime:

      handleUnauthorized (req: HttpRequest<any>, next: HttpHandler): Observable<any> {
            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);
                // get a new token via userService.refreshToken
                return this.userService.refreshToken()
                    .pipe(switchMap((newToken: string) => {
                        // did we get a new token retry previous request
                        if (newToken) {
                            this.tokenSubject.next(newToken);
                            return next.handle(this.addToken(req, newToken));
                        }
    
                        // If we don't get a new token, we are in trouble so logout.
                        this.userService.doLogout();
                        return observableThrowError('');
                    })
                        , catchError(error => {
                            // If there is an exception calling 'refreshToken', bad news so logout.
                            this.userService.doLogout();
                            return observableThrowError('');
                        })
                        , finalize(() => {
                            this.isRefreshingToken = false;
                        })
                    );
            } else {
                return this.tokenSubject
                    .pipe(
                        filter(token => token != null)
                        , take(1)
                        , switchMap(token => {
                            return next.handle(this.addToken(req, token));
                        })
                    );
            }
        }
    

    We have an attribute on the interceptor class which checks if there is already a refresh token request running: this.isRefreshingToken = true; because you don't want to have multiple refresh request when you fire multiple unauthorized requests.

    So everthing within the if (!this.isRefreshingToken) part is about refreshing your token and try the previous request again.

    Everything which is handled in else is for all requests, in the meantime while your userService is refreshing the token, a tokenSubject gets returned and when the token is ready with this.tokenSubject.next(newToken); every skipped request will be retried.

    Here this article was the origin inspiration for the interceptor: https://www.intertech.com/angular-4-tutorial-handling-refresh-token-with-new-httpinterceptor/

    EDIT:

    TokenSubject is actually a Behavior Subject: tokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);, which means any new subscriber will get the current value in the stream, which would be the old token from last time we call this.tokenSubject.next(newToken).

    Withnext(null) every new subscriber does not trigger the switchMap part, thats why filter(token => token != null) is neccessary.

    After this.tokenSubject.next(newToken) is called again with a new token every subscriber triggers the switchMap part with the fresh token. Hope it is more clearly now