1

I am using an Interceptor in my application, when the token is expired (401 returned), I want to refresh the token, save the new token to localstorage and then continue the request with the new token.

Catching the 401 error works, and I am able to get a new token but the request still fails with a 401 and doesn't work UNTIL I refresh the page.

This is the code for the intercept method currently using:

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  console.log('Interceptor called===============>');
  const token = localStorage.getItem(environment.TOKEN_NAME);

  if (token) {
    const headers = { Authorization: `Bearer ${token}` };
    Object.keys(AppHttpInterceptor.headers).forEach((header) => {
      // @ts-ignore
      if (!AppHttpInterceptor.headers[header]) {
        return;
      }
      // @ts-ignore
      headers[header] = AppHttpInterceptor.headers[header];
    });
    request = request.clone({ setHeaders: headers });
  }
  const handled: Observable<HttpEvent<any>> = next.handle(request);
  const subject: AsyncSubject<HttpEvent<any>> = new AsyncSubject();
  handled.subscribe(subject);
  subject.subscribe((event: HttpEvent<any>) => {
    if (event instanceof HttpErrorResponse) {
      if (event.status === 401) {
        return;
      }

      this.httpError.emit(event);
    }
  }, (err: HttpEvent<any>) => {
    if (err instanceof HttpErrorResponse) {
      if (err.status === 401) {
        this.tokenRefreshService.refreshToken().subscribe(response => {
          const headers = { Authorization: `Bearer ${localStorage.getItem(environment.TOKEN_NAME)}` };
          console.log('HEADERS ======> ' + headers);
          // @ts-ignore
          Object.keys(AppHttpInterceptor.headers).forEach((header) => {
            // @ts-ignore
            if (!AppHttpInterceptor.headers[header]) {
              return;
            }
            // @ts-ignore
            headers[header] = AppHttpInterceptor.headers[header];
          });
          request = request.clone({ setHeaders: headers });
        });

        return;
      }
      if (err.status === 404) {
        return;
      }
      this.httpError.emit(err);
    }
  });
  return Observable.create((obs: Observer<HttpEvent<any>>) => {
    subject.subscribe(obs);
  });
}

Is this the right approach?

Connor Low
  • 5,900
  • 3
  • 31
  • 52
Ojonugwa Jude Ochalifu
  • 26,627
  • 26
  • 120
  • 132
  • Why won't you use token silent refresh? Assuming you already get 401 it is not the best approach to refresh token. Also it is bad practice to subscribe in observables, if you want you should manipulate the stream – LukaszBalazy Mar 06 '21 at 10:28
  • Just so you could have a glimps take a look here: https://dev-academy.com/angular-jwt/ I don't know exactly what are you trying to do, but there is a logic that most likely you are tyring to implement `handle401Error` – LukaszBalazy Mar 06 '21 at 10:34
  • @LukaszBalazy Thanks for your response. I've taken a look at the silent refresh option, unfortunately, my authentication server runs behind Spring Security, and I can't configure it to work with OIDC – Ojonugwa Jude Ochalifu Mar 06 '21 at 17:02
  • You are doing it in a kind of weird way. You simply need some `catchError`/`retry*` operator, get there a new token and redo the request. In your interceptor it looks like you are doing some assignment of a request, but I don't see it being executed and then re-executed the original one – Sergey Mar 09 '21 at 17:53
  • @Sergey Can you provide a sample code? The code I posted is practically trail and error, hence the bounty. – Ojonugwa Jude Ochalifu Mar 09 '21 at 18:09
  • Looks like what you need https://stackoverflow.com/questions/45202208/angular-4-interceptor-retry-requests-after-token-refresh – Sergey Mar 09 '21 at 19:09
  • Refreshing token is solved issue, I would better recommend see a full working code and modify it if you don't like. Here you have 3 solutions: https://github.com/alexzuza/angular-refresh-token – Llorenç Pujol Ferriol Mar 10 '21 at 10:00

3 Answers3

2

Catching the 401 error works, and I am able to get a new token but the request still fails with a 401 and doesn't work UNTIL I refresh the page.

After getting a new token, you send the old (expired) one again or interceptor is not aware of new one to send. I don't see the right way to handle (replace) the new token. How do you replace the old Token after calling the refresh token API?

Let's take a look at the detailed diagram for complete implementation of interceptor :

enter image description here

As you see, the interceptor is a middleware between client requests and server with a cyclic implementation. Your interceptor is made in a kind of weird way to have a cyclic behavior. Any changes should be done immediately and deliberately inside it, otherwise it can disturb the application behavior. At least, you should let the interceptor remove/replace the expired token itself dealing with 401 status code, Although we may set tokens outside it (inside a service or even component)!

This is the simple & clean logic for interceptor implementation :

@Injectable()
export class TokenInterceptor implements HttpInterceptor {

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

    if (token) {
      // addToken to request
    }

    return next.handle(request).pipe(catchError(error => {
      if (error instanceof HttpErrorResponse && error.status === 401) {
        // handle401Error
      } else {
        // throwError
      }
    }));
  }
}

There are two ways to update the token which you can select based on your application working model :

  • working directly with localStorage commands including localStorage.removeItem(environment.TOKEN_NAME)
  • wrapping the above by taking advantage of BehaviorSubject which is preferred dealing with applications with refresh tokens. Read the Angular JWT Authorization with Refresh Token and Http Interceptor for a clean implementation with BehaviorSubject.

Update: A closer look to debug your approach

You're using AsyncSubject instead BehaviorSubject, So what is the difference? Actually, the AsyncSubject works a bit different where only the last value of the Observable execution is sent to its subscribers, and only when the execution completes. For this reason, using AsyncSubject is not prevalent subject to implement an interceptor.

For example, at the following steps, nothing happens using AsyncSubject:

  1. We create the AsyncSubject
  2. We subscribe to the Subject with Subscriber A
  3. The Subject emits 3 values, still nothing happens
  4. We subscribe to the subject with Subscriber B
  5. The Subject emits a new value, still nothing happens
  6. The Subject completes. Now the values are emitted to the subscribers which both log the value.

Let's come back to your code. The only line where you have a subscription to the AsyncSubject is subject.subscribe(obs);. Using AsyncSubject in this way only adds a latency to your interceptor. I'm not sure, but you may fix your approach by working a bit more, perhaps by something like below followed by the logic which I mentioned above:

next.handle(request).do(event => {
    if (event instanceof HttpResponse) {
        subject.next(event);
        subject.complete();
    }
}).subscribe(); // must subscribe to actually kick off request!
return subject;
Amirhossein Mehrvarzi
  • 18,024
  • 7
  • 45
  • 70
1

I can see 2 very small issues in your code that might be the reason it is not adding the new token unless you refresh.

  1. when status is 404 you are not returning anything you have null return

  2. you are updating the initial request it self during first clone, I would suggest you to keep the initial request param as it is and also handle the scenario when your refresh token fails.

        intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
          console.log('Interceptor called===============>');
          const token = localStorage.getItem(environment.TOKEN_NAME);
          if (token) {
            const headers = {Authorization: `Bearer ${token}`};
            Object.keys(AppHttpInterceptor.headers).forEach((header) => {
              // @ts-ignore
              if (!AppHttpInterceptor.headers[header]) {
                return;
              }
              // @ts-ignore
              headers[header] = AppHttpInterceptor.headers[header];
            });
           const  request = req.clone({setHeaders: headers}); // created  a new one intial req is not updated
          }
    
    
    
          return next.handle(request).pipe(catchError(error =>{
            if(error instanceof HttpErrorResponse){
              console.log((<HttpErrorResponse>error));
    
              if((<HttpErrorResponse>error).status === 401 && (<HttpErrorResponse>error).error.console.error_description.indexOf("Invalid refresh token" >= 0)){
                return observableThrowError(error); // if refresh token is exptired
              }
              if((<HttpErrorResponse>error).status === 401 && (<HttpErrorResponse>error).error.console.error_description.indexOf("Invalid_token" >= 0)){
                return this.handle401Error(req, next); //passing initial request
              }
    
    
              return observableThrowError(error);
            }
          }));
    
          handle401Error(req: HttpRequest<any>, next: HttpHandler){
    
            return  this.tokenRefreshService.refreshToken().pipe(
              switchMap(() =>{
                const headers = {Authorization: `Bearer ${localStorage.getItem(environment.TOKEN_NAME)}`};
                const newRequest =  req.clone({setHeaders: headers });
                return next.handle(newRequest); // return a new handle it will create a new request with updated header data
              }),
              catchError(error =>{
                retun observableThrowError(error);
              }),
              finalize(() =>{
                 // some extra lines
              }) // import the token service
            )
          }
    
        }
    

let me know if this resolves your issue. Thanks !

1

you need to rethink your approach on observables, and think as a stream that you chain, something like that

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
            return from(this.authenticationService.getAuthenticationToken()
                .then(token => {
                    return req.clone({
                        setHeaders: {
                            Authorization: `Bearer ${token}`
                        }
                    });
                })
                .catch(err => {
                    this.authenticationService.ErrorHandler(err);
                    return req;
                }))
                .switchMap(req => {
                    return next.handle(req);
                });
        }
lazizanie
  • 493
  • 3
  • 15