Dmitry
Dmitry

Reputation: 127

Can not make subscription in interceptor

I'm making JWT interceptor to handle 401 error. My idea is collect all original requests after 401 to array, then refresh my token, then add a new header to to my request. Here is the code:

type CallerRequest = {
    subscriber: Subscriber<any>;
    failedRequest: HttpRequest<any>;
};

@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {
  private refreshInProgress: boolean;
  private requests: CallerRequest[] = [];

  constructor(
    public authService: AuthService,
    public customersService: CustomersService,
    private http: HttpClient
    ) {
      this.refreshInProgress = false;
    }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (!req.url.includes('api/')) {
      return next.handle(req);
    }

  const observable = new Observable<HttpEvent<any>>((subscriber) => {
  const originalRequestSubscription = next.handle(req)
    .subscribe((response) => {
      subscriber.next(response);
    },
    (err) => {
      if (err.status === 401) {
        this.handleUnauthorizedError(subscriber, req);
      } else {
        subscriber.error(err);
      }
    },
    () => {
      subscriber.complete();
    });

    return () => {
      originalRequestSubscription.unsubscribe();
    };
  });
  return observable;
  }

  private addToken(request: HttpRequest<any>, token: string) {
    return request.clone({setHeaders: {'Authorization': `Bearer ${token}`}});
  }

  private handleUnauthorizedError(subscriber: Subscriber<any>, request: HttpRequest<any>) {
    this.requests.push({ subscriber, failedRequest: request });
    if (!this.refreshInProgress) {
      this.refreshInProgress = true;
      console.log('go refresh');
      this.authService.refreshToken()
        .pipe(
          finalize(() => {
            this.refreshInProgress = false;
          })
        )
        .subscribe(
          result => {
            console.log(result); // <-- I cannot get here
            this.repeatFailedRequests(this.authService.getAccessToken());

          },
        err => {
          console.log(err); // <-- And cannot get here
        }
      )

    }
  }

  private repeatFailedRequests(authHeader: string) {
    this.requests.forEach((c) => {
      const requestWithNewToken = this.addToken(c.failedRequest, authHeader);
      this.repeatRequest(requestWithNewToken, c.subscriber);
    });
    this.requests = [];
  }

  private repeatRequest(requestWithNewToken: HttpRequest<any>, subscriber: Subscriber<any>) {
    this.http.request(requestWithNewToken).subscribe((res) => {
      subscriber.next(res);
    },
      (err) => {
        if (err.status === 401) {
          this.authService.removeTokens();
        }
        subscriber.error(err);
      },
      () => {
        subscriber.complete();
      });
  }


}

Look at my handleUnauthorizedError method. I can't get results there.
By the way here is my refreshToken:

  refreshToken() {
    return this.post('api/v0/jwt/refresh/', {refresh: this.getRefreshToken()})
    .pipe(tap((tokens: Tokens) => {
      this.storeTokens(tokens);
    }));

}

What am I doing wrong and how would I solve this issue in a proper way? Thank you!

ADDED:

If I try JWT interceptor from this question Angular 4 Interceptor retry requests after token refresh (most popular answer) I get the same problem - my results from RefreshToken() are not handled.

Upvotes: 3

Views: 1076

Answers (2)

Dmitry
Dmitry

Reputation: 127

The problem was in proper handling JWT refresh requests while refreshInProgress. Here is the code which works just perfect:

type CallerRequest = {
    subscriber: Subscriber<any>;
    failedRequest: HttpRequest<any>;
};

@Injectable()
export class AuthTokenInterceptor implements HttpInterceptor {
  private refreshInProgress: boolean;
  private requests: CallerRequest[] = [];
  private refreshJWTReuest: Subscription;
  private straightRequests: string[];

  constructor(
    public authService: AuthService,
    public customersService: CustomersService,
    private http: HttpClient
    ) {
        this.refreshInProgress = false;
        this.straightRequests = ['currency/'];
      }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (this.straightRequests.some(v => req.url.includes(v))) {
      return next.handle(req);
    }

    if (req.url.includes('api/v0/jwt/create/')) {
      this.refreshInProgress = false;
    }


  const observable = new Observable<HttpEvent<any>>((subscriber) => {
  const originalRequestSubscription = next.handle(this.addToken(req, this.authService.getAccessToken()))
    .subscribe((response) => {
      subscriber.next(response);
    },
    (err) => {
      if (err.status === 401) {
        if (req.url.includes('api/v0/jwt/refresh/')) {
          this.refreshJWTReuest.unsubscribe();
          this.requests = [];
          this.authService.logout();
        }
        this.handleUnauthorizedError(subscriber, req);
      } else {
        // notification!!!
        subscriber.error(err);
      }
    },
    () => {
      subscriber.complete();
    });

    return () => {
      originalRequestSubscription.unsubscribe();
    };
  });
  return observable;
  }

  private addToken(request: HttpRequest<any>, token: string) {
    return request.clone({setHeaders: {'Authorization': `Bearer ${token}`}});
  }

  private handleUnauthorizedError(subscriber: Subscriber<any>, request: HttpRequest<any>) {
    if (!this.refreshInProgress) {
      this.refreshInProgress = true;
      this.refreshJWTReuest = this.authService.refreshToken()
        .subscribe(
          result => {
            this.refreshInProgress = false;
            this.repeatFailedRequests(result.access);
          },
        err => {
          this.authService.logout();
          console.log(err);
        }
      );

    }
    if (!request.url.includes('api/v0/jwt/refresh/')) {
      // avoid refresh requests while refreshInProgress
      // this solves a small bug after logout()
      this.requests.push({ subscriber, failedRequest: request });
    }

  }

  private repeatFailedRequests(authHeader: string) {
    this.requests.forEach((c) => {
      const requestWithNewToken = this.addToken(c.failedRequest, authHeader);
      this.repeatRequest(requestWithNewToken, c.subscriber);
    });
    this.refreshJWTReuest.unsubscribe();
    this.requests = [];
  }

  private repeatRequest(requestWithNewToken: HttpRequest<any>, subscriber: Subscriber<any>) {
    this.http.request(requestWithNewToken).subscribe((res) => {
      subscriber.next(res);
    },
      (err) => {
        if (err.status === 401) {
          this.authService.logout();
        }
        subscriber.error(err);
      },
      () => {
        subscriber.complete();
      });
  }

}

Upvotes: 1

Berk Kurkcuoglu
Berk Kurkcuoglu

Reputation: 1523

I think the problem is with your intercept method as you are wrapping the originalObservable it doesn't get called properly.

Try something like this:

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (!req.url.includes('api/')) {
      return next.handle(req);
    }

   return next
      .handle(req)
      .catch((err) => {
          if (err.status === 401) {
            this.handleUnauthorizedError(req);
          } else {
            return next.handle(req);
          }
        })
      );    
  }

Upvotes: 0

Related Questions