Search code examples
angulartypescriptjwtrefresh-token

Renew token before making cloned request


So I want to renew access token if the token is expired using refresh token and this is what my token interceptor looks like:

intercept(req: HttpRequest<any>, next: HttpHandler) {
    let authservice = this.injector.get(AuthService)
      let tokenreq = req.clone({
        setHeaders: {
          Authorization: `Bearer ${authservice.setToken()}`
        }
      })
      return next.handle(tokenreq).pipe(
        catchError((error) => {
          if (error.error.message == 'The incoming token has expired') {
            authservice.renewToken()
            let tokenreq = req.clone({
              setHeaders: {
                Authorization: `Bearer ${authservice.setToken()}`
              }
            })
            return next.handle(tokenreq)
          }
          else return throwError(error)
        })
      );
    }
  }

The problem is after I get the expired token message the authservice.renewToken() and authservice.setToken() were called at the same time so the expired token was set again.

Another problem is if the user opens the application again with the expired token in cookies all the GET method will throw an error and will request for new token multiple times. How can I handle expire token error?


Solution

  • You can fix this fault behavior by connecting setToken to the returned observable by using retryWhen operator. This way renewToken and setToken will not be executed in parallel AND more importantly setToken will be take into consideration by the interceptor chain in EACH request.

    intercept(req: HttpRequest<any>, next: HttpHandler) {
        const authservice = this.injector.get(AuthService);
          
        return of(req).pipe(
              switchMap((req) => {
                return authservice.setToken()               // Gets the token. *should rename this method to getToken()
                    .pipe(
                        map(token => {                      // Set the token.
                            const tokenreq = req.clone({
                                setHeaders: {
                                    Authorization: `Bearer ${authservice.setToken()}`
                                }
                                });
    
                            return tokenreq;
                        })
                    ),
                switchMap(tokenreq => next.handle(tokenreq)), // Execute next interceptor and eventually send the request.
                retryWhen(errors => errors.pipe(
                    mergeMap((err: HttpErrorResponse, i: number) => {
                        authservice.invalidateToken()        // Invalidate token. Erase token or expires it.
                        if (error.error.message == 'The incoming token has expired') {
                            return of(err);                   // will start the current pipe all over again - and get the token once again.
                        }
    
                        return throwError(error);
                    })
                )
            )
      }
    

    Explanation:

    In the question, setToken isn't connected to the observables chain that are returned by interceptors method. The flow of code execution before:

    1) interceptor method execution -> setToken() -> return observable
    2) request asked by http.get(..) -> chain of observables return by interceptors -> request sent -> chain of observables
    

    And in this answer:

    1) interceptor method execution -> setToken() -> return observable
    2) request asked by http.get(..) -> chain of observables return by interceptors and setToken() inside one! -> request sent -> chain of observables
    

    Note:

    setToken method should return an observable with the token and invalidateToken should be able to delete the token.

    This could be easily achieved by:

    private token$: AsyncSubject<string>;
    
    getToken(): Observable {
        if (!token$) this.token$ = new AsyncSubject();
    
        getTokenOperation.subscribe(t => {
            this.token$.next(t);
            this.token$.complete();
        })
    
        return this.token$.asObservable();
    }
    
    invalidateToken() {
        this.token$ = null;
    }