Renew token before making cloned request

Viewed 340

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?

2 Answers

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;
}

you need to pass the new token.

return next.handle(request).pipe(
        map((event: HttpEvent<any>) => {
            return this.eventCallSendBack(event);
        }),
        catchError((error: HttpErrorResponse) => {
            if (error instanceof HttpErrorResponse) {
                switch (error.status) {
                    case 401:
                        return this.handle401Error(request, next);
                    case 403:
                        //DO Want you want here.
                        throw error;
                    default:
                        throw error;
                }
            }
            throw error;
        })
    );

This is the callback fn.

private eventCallSendBack(event) {
    if (event instanceof HttpResponse) {
        return event.clone({ body: event.body });
    }
    return event;
}

You would to like to have and an add token fn:

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

And now let say that your token is expired you need to create fn that will get the new token form some Auth.

private handle401Error(
    request: HttpRequest<any>,
    next: HttpHandler
): Observable<any> {
        return authServices
            .refreshToken(
                token //This is the old token to get the new.
            )
            .pipe(
                map(jwt => {
                    return jwt;
                }),
                switchMap((jwt: any) => {
                    return next
                        .handle(this.addToken(request, jwt.token)) // Add token in responce 
                        .pipe(
                            map((event: HttpEvent<any>) => {
                                return this.eventCallSendBack(event);
                            })
                        );
                }),
                catchError(err => {
                    return throwError(err);
                })
            );
    
}
Related