3
votes

I'm working on an Ionic app and trying to cash in the refresh token when a user gets a 401 response on an HTTP request. I found a few examples floating around online and was able to get this one (https://www.intertech.com/Blog/angular-4-tutorial-handling-refresh-token-with-new-httpinterceptor/) working with the exception of multiple requests coming in at once.

The problem I'm having is the first call in the series of calls invokes the refresh token and retries successfully, while the other ones never get retried. If I take the .filter and .take off the subject return for requests where a refresh is already in progress, the calls do get retried but without the new token. I'm pretty new when it comes to observables and subjects so I'm not really sure what the problem could be.

requests

  this.myService.getData().subscribe(response => {this.data = response.data;}); 
  this.myService.getMoreData().subscribe(response => {this.moreData = response.data;}); 
  this.myService.getEvenMoreData().subscribe(response => {this.evenMoreData = response.data;}); 

interceptor

@Injectable()
export class HttpInterceptor implements HttpInterceptor {

  isRefreshingToken: boolean = false;
  tokenSubject = new BehaviorSubject<string>(null);   

  tokenService: tokenService;

  constructor(private authService: AuthService, private injector: Injector) { }

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

    return this.authService.getUser().flatMap(user => {
      request = this.addToken(request, next, user.accessToken);

      return next
        .handle(request)
        .catch(error => {
          if (error instanceof HttpErrorResponse) {
            switch ((<HttpErrorResponse>error).status) {
              case 401:
                return this.handle401(request, next, user);
            }
          } else {
          return Observable.throw(error);
        };
      })
    });


  }

  addToken(request: HttpRequest<any>, next: HttpHandler, accessToken: string): HttpRequest<any> {
    return request.clone({ setHeaders: { Authorization: 'Bearer ' + accessToken }})
  }

  handle401(request: HttpRequest<any>, next: HttpHandler, user: any) {

    if (!this.isRefreshingToken) {
      this.isRefreshingToken = true;
      this.tokenSubject.next(null);
      this.tokenService = this.injector.get(tokenService);
      return this.tokenService.refresh(user.refreshToken)
        .switchMap(refreshResponse => {
          if (refreshResponse) {
            this.authService.setUser(refreshResponse.id_token, refreshResponse.access_token, refreshResponse.refresh_token);
            this.tokenSubject.next(refreshResponse.accessToken);
            return next.handle(this.addToken(request, next, refreshResponse.access_token));
          }
          else {
             //no token came back. probably should just log user out.
          }
        })
        .finally(() => {
          this.isRefreshingToken = false;
      });      
    }
    else {
      return this.tokenSubject
        .filter(token => token != null)
        .take(1)
        .switchMap(token => {
          return next.handle(this.addToken(request, next, token));
        });
    }

  }



}
3

3 Answers

0
votes

I actually ended up solving this by moving the subject to my auth service and doing a next in the setUser method. Then in the else statement in my 401 method, I returned the subject from a new method on my auth service and that fixed it. I still needed the take(1) but was able to get rid of the filter since I ended up not using a BehaviorSubject.

0
votes

It looks to me like you didn't have the right token:

  • You had:

    this.tokenSubject.next(refreshResponse.accessToken);

  • Should be:

    this.tokenSubject.next(refreshResponse.access_token);

0
votes

I faced a similar issue in the past. For some unknown reason (at least to me), when I intercept the 401, I make the refresh and retry, but retry operation goes cancelled.

Nevertheless, I realised that I can read the JWT expiration on client-side, so I tricked the system by saving the token expiration time. I then made routing events (say onViewWillEnter) check the expiration and, if token expired, refresh it.

This mechanism is totally transparent to the user, ensures that auth token nor refresh token expire if the user stays too long without performing HTTP requests and, most importantly, reduces latencies as you never get a 401 response (which, in your scenario, translates to three requests).

One simple way to achieve this is by means of a guard:

canActivate(route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot) {
    if (this.refreshTokenService.isExpired) {
        this.tokenEvent_.next();
        return false;
    } else {
        this.refreshTokenService.refresh();
    }

where refreshTokenService is a utility service that has the tokens and a method for performing refresh via HTTP. tokenEvent is a rxjs/Subject: it is subscribed in guard constructor and each time a new event comes, it redirects to login page.

Adding this guard on every route ensures that the token is always non-expired.