Angular 4 Interceptor Retry Requests After Token Refresh

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



Leave a reply



Submit