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