Reputation: 799
I've browsed a ton of articles & questions but none seem to cover automatic token refresh in an interceptor WHILE also having a route guard that waits for a request to* the server to complete to verify that the access token is valid?
auth.guard.ts
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.authService.isAuthenticated()
.pipe(
map(
res => {
if (res.success === true) {
return true;
} else {
this.authService.deleteToken();
this.router.navigateByUrl('/login');
return false;
}
}
), catchError(err => {
// NOTE: If I put the code for refresh-token request here, it works -- user stays
// logged in after getting new access token
this.authService.deleteToken();
this.router.navigate(['/login']);
return of(false);
})
);
}
auth.interceptor.ts
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.headers.get('noauth')) {
return next.handle(req.clone());
} else {
const clonedReq = req.clone({
headers: req.headers.set('Authorization', 'Bearer ' + this.authService.getToken())
});
return next.handle(clonedReq).pipe(
tap(
event => {},
err => {
if (err.error.code && err.error.code === 'EXPIRED') {
return this.handle401Error(clonedReq, next);
}
}
)
);
}
}
private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
return this.authService.refreshToken().pipe(
switchMap((token: any) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(token.new_access_token);
return next.handle(this.addToken(request, token.new_access_token));
}),
catchError(err => {
return throwError(err);
}))
.subscribe(r => r);
} else {
return this.refreshTokenSubject.pipe(
filter(token => token != null),
take(1),
switchMap(jwt => {
return next.handle(this.addToken(request, jwt));
}));
}
}
private addToken(request: HttpRequest<any>, token: string) {
return request.clone({
setHeaders: {
'Authorization': `Bearer ${token}`
}
});
}
auth.service.ts
refreshToken() {
return this.http.post<any>(`${environment.api}/refresh-token`, {
'refreshToken': this.getRefreshToken()
}, this.noAuthHeader).pipe(tap((res) => {
if (res.success === true) {
this.setToken(res.new_access_token);
return true;
} else {
this.deleteToken();
this.router.navigateByUrl('/login');
return false;
}
}));
}
What happens is:
I'm not sure how it's supposed to happen but what I'm thinking is that the request for getting a new access token should get completed first, and then the authentication request/auth.guard should base on that?
Upvotes: 1
Views: 3391
Reputation: 799
After being stuck for a day and a half, I think I finally fixed it on my own:
auth.guard.ts
return this.authService.isAuthenticated()
.pipe(
map(
res => {
if (res.success === true) {
return true;
} else {
this.authService.deleteToken();
this.router.navigateByUrl('/login');
return false;
}
}
), catchError(
err => {
// Absolutely needed this handler,
// but removed the code for redirection to login and deletion of tokens
return of(false);
}
)
);
auth.interceptor.ts
return next.handle(clonedReq)
.pipe(
catchError((err: HttpErrorResponse) => {
if (err.error.code === 'EXPIRED') {
return this.handle401Error(clonedReq, next);
} else if (err.error.status === 401) {
this.router.navigateByUrl('/login');
return throwError(err.error.message);
}
// Redirect to an error landing page
return throwError(err.error.message);
})
);
private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
return this.authService.refreshToken()
.pipe(
switchMap((token: any) => {
this.refreshTokenSubject.next(token.new_access_token);
this.authService.setToken(token.new_access_token);
return next.handle(this.addToken(request, token.new_access_token));
}), catchError(err => {
// .subscribe() not needed!Just an error handler
if (err.error.code === 'EXPIRED') {
this.authService.deleteToken();
this.router.navigateByUrl('/login');
return throwError(err.error.message);
}
// Redirect to error landing page
return throwError(err.error.message);
}),
finalize(() => {
this.isRefreshing = false;
})
);
...
I hope you find this helpful!
Upvotes: 2