angular-7-interceptor-retry-requests-after-token-refresh
Replying late. this is how I did it.
Works with parallel request as well.
import { HttpInterceptor, HttpRequest, HttpHandler, HttpErrorResponse, HttpEvent, HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { GlobalService } from './global.service';
import { tap, mergeMap, switchMap, filter, take, flatMap, catchError } from 'rxjs/operators';
import { Observable, Subject, throwError, of, BehaviorSubject } from 'rxjs';
@Injectable()
export class InterceptorService implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(public globalService: GlobalService) {
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
request = this.addHeader(request);
// console.log('request ', request);
return next.handle(request).pipe(catchError((error: any) => {
if (error instanceof HttpErrorResponse) {
if (error.status === 401) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
request = this.addHeader(request);
console.log('Token request');
return this.globalService.getRefershToken().pipe(
switchMap((token: any) => {
console.log('inside token');
this.isRefreshing = false;
if (token && token.access_token && token.refresh_token) {
sessionStorage.setItem('accessToken', token.access_token);
sessionStorage.setItem('refreshToken', token.refresh_token);
}
// request = this.addHeader(request);
// console.log('request: ', request);
this.refreshTokenSubject.next(token);
return next.handle(this.addHeader(request));
}));
} else {
console.log('inside else call');
console.log('token : ', this.refreshTokenSubject.getValue());
return this.refreshTokenSubject.pipe(
filter(token => (token != null && token != undefined)),
take(1),
switchMap(() => {
console.log('adding header in else');
return next.handle(this.addHeader(request))
}));
}
}
}
}));
}
private addHeader(request: HttpRequest<any>) {
let getEndPoint = request.url.split('/')[request.url.split('/').length - 1];
if (getEndPoint !== 'refreshToken') {
const accessToken = sessionStorage.getItem('accessToken');
request = request.clone({
setHeaders: {
Authorization: `Bearer ${accessToken}`
}
});
} else if (getEndPoint === 'refreshToken') {
const refreshToken = sessionStorage.getItem('refreshToken');
request = request.clone({
setHeaders: {
applicationCode: environment.applicationCode,
'refresh-token': `${refreshToken}`,
}
});
}
return request;
}
}
Angular JWT retry REST call after refresh token with multiple interceptors
MY SOLUTION
EDIT error.interceptor:
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private authService: AuthService, private router: Router, private toastr: ToastrService) {
}
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<Object>> {
let authReq = req;
const token = this.authService.getToken();
const authToken = this.authService.getToken();
const username = localStorage.getItem('username');
if (token != null) {
authReq = this.addTokenHeader(req);
}
return next.handle(authReq).pipe(catchError(error => {
if (error instanceof HttpErrorResponse && (error.error.code == '40102' || error.error.error == 'unauthorized_client')) {
return this.handle401Error(authReq, next);
}
return throwError(() => error);
}));
}
private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
return this.authService.refreshToken().pipe(
switchMap((res: any) => {
localStorage.setItem('access_token', res.access_token);
this.authService.verifyToken(res.access_token).subscribe((resVer: any) => {
localStorage.setItem('username', resVer.sub);
this.isRefreshing = false;
this.refreshTokenSubject.next(res.access_token);
})
return next.handle(this.addTokenHeader(request));
}),
catchError((err) => {
this.isRefreshing = false;
return throwError(() => err);
})
);
}
return this.refreshTokenSubject.pipe(
filter(token => token !== null),
take(1),
switchMap((token) => next.handle(this.addTokenHeader(request)))
);
}
private addTokenHeader(request: HttpRequest<any>) {
const authToken = this.authService.getToken();
const username = localStorage.getItem('username');
if (authToken && username) {
return request = request.clone({
setHeaders: {
'Authorization': `Bearer ${authToken}`,
'OAM_REMOTE_USER': username,
'rejectUnauthorized': 'false'
}
})
} else {
return request;
}
}
}
Trying to repeat a http request after refresh token with a interceptor in angular 7
You have to distingiush among all the requests. For example you don't want to intercept your login request and also not the refresh token request. SwitchMap is your best friend because you need to cancel some calls to wait for your token is getting refreshed.
So what you do is check first for error responses with status 401 (unauthorized):
return next.handle(this.addToken(req, this.userService.getAccessToken()))
.pipe(catchError(err => {
if (err instanceof HttpErrorResponse) {
// token is expired refresh and try again
if (err.status === 401) {
return this.handleUnauthorized(req, next);
}
// default error handler
return this.handleError(err);
} else {
return observableThrowError(err);
}
}));
In your handleUnauthorized function you have to refresh your token and also skip all further requests in the meantime:
handleUnauthorized (req: HttpRequest<any>, next: HttpHandler): Observable<any> {
if (!this.isRefreshingToken) {
this.isRefreshingToken = true;
// Reset here so that the following requests wait until the token
// comes back from the refreshToken call.
this.tokenSubject.next(null);
// get a new token via userService.refreshToken
return this.userService.refreshToken()
.pipe(switchMap((newToken: string) => {
// did we get a new token retry previous request
if (newToken) {
this.tokenSubject.next(newToken);
return next.handle(this.addToken(req, newToken));
}
// If we don't get a new token, we are in trouble so logout.
this.userService.doLogout();
return observableThrowError('');
})
, catchError(error => {
// If there is an exception calling 'refreshToken', bad news so logout.
this.userService.doLogout();
return observableThrowError('');
})
, finalize(() => {
this.isRefreshingToken = false;
})
);
} else {
return this.tokenSubject
.pipe(
filter(token => token != null)
, take(1)
, switchMap(token => {
return next.handle(this.addToken(req, token));
})
);
}
}
We have an attribute on the interceptor class which checks if there is already a refresh token request running: this.isRefreshingToken = true;
because you don't want to have multiple refresh request when you fire multiple unauthorized requests.
So everthing within the if (!this.isRefreshingToken)
part is about refreshing your token and try the previous request again.
Everything which is handled in else
is for all requests, in the meantime while your userService is refreshing the token, a tokenSubject gets returned and when the token is ready with this.tokenSubject.next(newToken);
every skipped request will be retried.
Here this article was the origin inspiration for the interceptor: https://www.intertech.com/angular-4-tutorial-handling-refresh-token-with-new-httpinterceptor/
EDIT:
TokenSubject is actually a Behavior Subject: tokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);
, which means any new subscriber will get the current value in the stream, which would be the old token from last time we call this.tokenSubject.next(newToken)
.
Withnext(null)
every new subscriber does not trigger the switchMap
part, thats why filter(token => token != null)
is neccessary.
After this.tokenSubject.next(newToken)
is called again with a new token every subscriber triggers the switchMap
part with the fresh token. Hope it is more clearly now
EDIT 21.09.2020
Fix link
Retry a subscription in component after HTTP Interceptor successful refresh the token and do the request
just add return before you return your handling tricks. without return the logic just doesn't work.
if (error instanceof HttpErrorResponse && error.status === 401) {
return this.handle401Error(request, next,token);
angular http interceptor not calling request again after token refresh
The problem
this.auth.refreshAccessToken()
returns a promise (I assume given the .then()
).
Explanation
Just in case you are not familiar with promises, they are a common system for handling asynchronous code. Here is a link to the docs.
The this.auth.refreshAccessToken().then()
takes a function as an argument, as is common, you have provided an anonymous arrow function (token: Token) => { ... }
.
When you do return next.handle(this.addToken(request, token.access_token));
, you are inside the arrow function, so you are not actually returning a value from handle401Error()
, you are returning a value to .then()
.
.then()
does return a value, but you aren't returning that at the moment.
You can see this done correctly in your else block:
return this.refreshTokenSubject.pipe( <-- top-level return
filter((token) => token !== null),
take(1),
switchMap((token) => {
return next.handle(this.addToken(request, token)); <-- nested return
}));
}
The solution
TLDR;
return from(this.auth.refreshAccessToken()).pipe(switchMap((token: Token) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(token.access_token);
return next.handle(this.addToken(request, token.access_token));
}));
Explanation
A small thing that might make things easier, I would recommend instead of any
as the return type of handle401Error()
you use the return type of handle.next()
which is Observable<HttpEvent<any>>
.
What you need to do is return the value of next.handle()
from inside this.auth.refreshAccessToken().then()
.
There are probably multiple ways to do this, but I'm going to recommend the Angular/RxJS style.
As I said before, promises are like observables and RxJS (v6+) provides a way to convert a promise to an observable, for example:
import { from } from 'rxjs';
const observable = from(promise);
You can use this to convert this.auth.refreshAccessToken()
to an observable:
from(this.auth.refreshAccessToken())
Now we have an observable, you might be inclined to get the value out using subscribe
but that is not what you want to do because your interceptor is returning a final observable that gets subscribed to elsewhere.
What you can do instead is use pipe, which allows you to use a number of operators provided by RxJS. In this case, you want to wait for your first observable refreshAccessToken()
to emit and then you want to return next.handle()
. The commonly used operator for this task is switchMap.
You will notice that your else block is actually using this:
return this.refreshTokenSubject.pipe(
filter((token) => token !== null),
take(1),
switchMap((token) => { <-- switchMap
return next.handle(this.addToken(request, token));
}));
}
switchMap()
waits for the first observable to emit, and then outputs the value into your callback function, expecting you to return another observable. In you case, this would mean that your replace then()
with pipe(switchMap())
.
As shown in the TLDR:
return from(this.auth.refreshAccessToken()).pipe(switchMap((token: Token) => {
this.isRefreshing = false;
this.refreshTokenSubject.next(token.access_token);
return next.handle(this.addToken(request, token.access_token));
}));
This should resolve your issue, please comment below if this doesn't work.
Angular HTTP Interceptor wait http requests until get a refresh token
I have solved the problem using the code below. If anyone has any better solution please suggest it.
private isRefreshingToken = false;
private timeOut = appSettings.ajaxTimeout;
private tokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
timeout(this.timeOut),
catchError((error) => {
if (error instanceof HttpErrorResponse) {
const httpErrorCode: number = error['status'];
switch (httpErrorCode) {
case StatusCodes.BAD_REQUEST:
return throwError(error);
case StatusCodes.UNAUTHORIZED:
return this.handle401Error(request, next);
default:
this._toastr.error(
'Sorry! something went wrong.',
'Error!'
);
return throwError(error);
}
} else {
return throwError(error);
}
})
);
}
private handle401Error(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
if (!this.isRefreshingToken) {
this.isRefreshingToken = true;
// Reset here so that the following requests wait until the token
// comes back from the refreshToken call.
this.tokenSubject.next(null);
return this._spotify.getRefreshToken().pipe(
switchMap((response: ISpotifyTokens) => {
console.log('updating on 401');
// Updating new token in cookie
this._spotify.updateTokensInStorage(response, false);
this.tokenSubject.next(response.access_token);
return next.handle(
this.addTokenInHeader(request, response.access_token)
);
}),
catchError((error) => {
// If there is an exception calling 'refreshToken', bad news so logout.
this.logoutUser();
return throwError('');
}),
finalize(() => {
this.isRefreshingToken = false;
})
);
} else {
return this.tokenSubject.pipe(
filter((token) => token != null),
take(1),
switchMap((token) => {
return next.handle(this.addTokenInHeader(request, token));
})
);
}
}
addTokenInHeader(
request: HttpRequest<any>,
token: string
): HttpRequest<any> {
return request.clone({
setHeaders: { Authorization: 'Bearer ' + token }
});
}
logoutUser(): void {
this._spotify.clearTokensFromStorage();
this._router.navigate(['/welcome']);
}
Related Topics
Capture Browser Console Logs with Capybara
Wkwebview - Complex Communication Between JavaScript & Native Code
Creating a "Sticky" Fixed-Position Item That Works on iOS Safari
What Are "Top Level JSON Arrays" and Why Are They a Security Risk
How to Execute Array of Promises in Sequential Order
Ios: Authentication Using Xmlhttprequest - Handling 401 Response
IE8 Var W= Window.Open() - "Message: Invalid Argument."
Converting JSON Format to CSV to Upload Data Table in R to Produce D3 Bubble Chart
Override Console.Log(); for Production
Using Enter Key with Action Button in R Shiny
Passing Array of Objects from Js to Rails
What Is the "Best" Way to Get and Set a Single Cookie Value Using JavaScript
Deserializing a JSON into a JavaScript Object
Shiny App:Disable Downloadbutton
How to Detect Faces Using Ruby
How Does JavaScript Determine the Number of Digits to Produce When Formatting Floating-Point Values