3
votes

My application uses a JWT Token to authenticate with the backend. I have an angular Interceptor that catches any errors in an http call. This interceptor checks if the call returns a 401 Unauthorized response and if so, it makes a call to an endpoint to refresh the token and then retries the original call again.

My issue is that the call to the refreshToken endpoint is being done twice for some reason.

Here's my interceptor (edited for brevity):

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((error: any) => {
        switch (error.status) {
          // unauthorized
          case 401:
            // the token is stored in an NGRX store, so retrieve it first
            return this.userStore.pipe(select(fromUser.getUserAccessToken)).pipe(
              take(1),
              mergeMap(userToken => {
                if (userToken) {
                  // token exists but may be invalid, try to refresh
                  return this.userService.refreshToken(userToken).pipe(
                    switchMap(refreshedToken => {
                      if (refreshedToken) {
                        ...
                      } else {
                        // token was not refreshed, delete session
                        ...
                      }
                    })
                  );
                }
              })
            );
        }
      })
    );
  }

return this.userService.refreshToken(userToken) is the statement that gets executed twice (using the same, original token), so the first call to refresh will go through, and the second call will obviously fail since its trying to refresh the same token again, resulting in the user session getting deleted.

Any ideas why?

Update: I'm still digging through this. I've put a bunch of breakpoints and console.logs in my code:

      catchError((error: any) => {
        >>>>> console.log('1');
        switch (error.status) {
          case 401:
            return this.userStore.pipe(select(fromUser.getUserAccessToken)).pipe(
              take(1),
              mergeMap(userToken => {
                >>>>> console.log('2');
                if (userToken) {
                  return this.userService.refreshToken(userToken).pipe(
                    switchMap(refreshedToken => {
                      >>>>> console.log('3');

Console.log #1 and #2 get printed out to the console just once, which means that the http error is only being caught once. But #3 gets printed twice, which I figure means either the call to the refresh endpoint is being made twice, or the observable is returning two values somehow?

3
Is the page that is triggering the refresh doing more than one call to your API? One thing I've done with this process is to queue up any 401's that come in after the initial one while the refresh is in progress. - John Graham
Do you have two requests being made? Maybe you're calling two endpoints? I had lots of errors based on JWT spring backend and Angular frontend. If you can show the component code too would help. - Jorge Mussato
@JohnGraham this particular component is only making a single http call. How do you go about queuing up additional 401 responses? That sounds like just what I need - mduck
@JorgeMussato just a single http request is being made! - mduck

3 Answers

3
votes

Not sure why, but it seems like refreshToken outputs multiple values. This can easily be fix by the take operator. Something like this:

 return this.userService.refreshToken(userToken).pipe(
                take(1),
                switchMap(refreshedToken => {
                  >>>>> console.log('3');
0
votes

Your backend call could be an OPTIONS request

Preflighted requests

Unlike simple requests (discussed above), "preflighted" requests first send an HTTP OPTIONS request header to the resource on the other domain, in order to determine whether the actual request is safe to send. Cross-site requests are preflighted like this since they may have implications to user data. In particular, a request is preflighted if:

It uses methods other than GET or POST. Also, if POST is used to send request data with a Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain, e.g. if the POST request sends an XML payload to the server using application/xml or text/xml, then the request is preflighted. It sets custom headers in the request (e.g. the request uses a header such as X-PINGOTHER)

0
votes

First, you really need to provide a StackBlitz or something similar with the bug so we can help diagnose this. That being said though, I was curious about this because I have had problems like this before so I did some research. Most of the examples I found show that the Observable returned by the refreshToken method is shared to prevent multiple subscriptions. In this answer https://stackoverflow.com/a/52720791/5799931 you can see that the this.authService.refreshToken() Observable is shared. I would try this

return this.userService.refreshToken(userToken).pipe(
       share(), <----- ADD THE SHARE HERE
       switchMap(refreshedToken => {
          if (refreshedToken) {
             ...
          } else {
            // token was not refreshed, delete session
            ...
          }
       })
   );

You can also share the Observable returned by HttpClient inside the refreshToken method.