Before each request a HTTP interceptor checks if the access token is expired and if so, it first renews the token and then continues the request.
The problem is on pages that have multiple requests. On those pages the interceptor tries to renew the token for each request.
How can I share the token renewal request in this case?
The method that is responsible for token renewal is this:
renewToken() {
let refreshToken = // get refresh token
let accessToken = // get access token
if (!refreshToken || !accessToken)
return;
let requestData = {
accessToken: accessToken,
refreshToken: refreshToken
}
return this.http.post<ApplicationToken>(`${this.baseUrl}/renewToken`, requestData)
.pipe(
// I tried share() shareReplay(1) and publishReplay(1) here but no luck
);
}
And this is how the interceptor uses that method:
...
// in case we need to renew the token
return this.accountService.renewToken()
.pipe(
switchMap(t => {
this.accountService.saveToken(t);
token = this.accountService.getAccessToken();
var newReq = this.setToken(req, token);
return next.handle(newReq);
})
);
...
You need to check if refresh token request is in progress or not because you don’t want other calls to come in and call refreshToken again.
Here I've created RefreshTokenInterceptor class for you.
You just need to customize it and follow the comments:
import { Injectable } from "@angular/core";
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler } from "@angular/common/http";
import { BehaviorSubject, Observable } from "rxjs";
import { catchError, switchMap, filter, take } from "rxjs/operators";
@Injectable({
providedIn: 'root'
})
export class RefreshTokenInterceptor implements HttpInterceptor {
private refreshTokenInProgress: boolean = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(
null
);
constructor(public accountService: AccountService) {}
intercept(request: HttpRequest<any>,next: HttpHandler): Observable<HttpEvent<any>> {
// Check first if token has expired
// If not, just add addAuthenticationToken();
// If expired
if (tokenHasExpired()) {
if (this.refreshTokenInProgress) {
// If refreshTokenInProgress is true, we will wait until refreshTokenSubject has a non-null value
// – which means the new token is ready and we can retry the request again
return this.refreshTokenSubject.pipe(
filter(result => result !== null),
take(1),
switchMap(() => next.handle(this.addAuthenticationToken(request)))
);
} else {
this.refreshTokenInProgress = true;
// Set the refreshTokenSubject to null so that subsequent API calls will wait until the new token has been retrieved
this.refreshTokenSubject.next(null);
return this.accountService.renewToken()
.pipe(
switchMap(t => {
this.accountService.saveToken(t);
let token = this.accountService.getAccessToken();
this.refreshTokenInProgress = false; // Set refreshTokenInProgress to False
this.refreshTokenSubject.next(token); // Add token to the refreshTokenSubject
var newReq = this.setToken(req, token);
return next.handle(newReq);
}),
catchError((err) => {
this.refreshTokenInProgress = false;
return Observable.throw(err);
})
);
}
} else {
return this.addAuthenticationToken(request);
}
}
addAuthenticationToken(request) {
// Get access token from Local Storage
const accessToken = this.accountService.getAccessToken();
// If access token is null this means that user is not logged in
// And we return the original request
if (!accessToken) {
return request;
}
// We clone the request, because the original request is immutable
return request.clone({
setHeaders: {
Authorization: accessToken
}
});
}
}