12

I'm trying to automate the refresh token requests upon receiving an error 401 with angular 7.

Between that I do not find much documentation of how to do it with angular 7 and that I do not have previous knowledge of angular or rxjs I am becoming a little crazy

I think it's almost completed, but for some reason the second next.handle(newReq) dont send the request (in google chrome network debugger only apears first request)

i'm gettin the response of refresh and making processLoginResponse(res) correctly

you can see here my interceptor

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

let newReq = req.clone();

return next.handle(req).pipe(
  catchError(error => {
    if (error.status == 401) {
      this._authenticationService.refresh().subscribe(
        res => {
          this._authenticationService.processLoginResponse(res);
          newReq.headers.set("Authorization", "Bearer " + this._authenticationService.authResponse.token)
          return next.handle(newReq)
        },
        error => {
          this._authenticationService.logOut();
        });
    }
    throw error;
  })
);
  • Possible duplicate of [Angular 4 Interceptor retry requests after token refresh](https://stackoverflow.com/questions/45202208/angular-4-interceptor-retry-requests-after-token-refresh) – massic80 Jun 17 '19 at 14:01

3 Answers3

20

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

J. S.
  • 2,268
  • 1
  • 12
  • 27
  • Why do you use `this.tokenSubject.next(null)` ? Doesn't it work without it? What that does if I understand correctly, is put a null into the event stream - but subscribers are ignoring null anyway, so what is the point of it? – lonix Jul 02 '19 at 18:00
  • 1
    @Ionix see my EDIT – J. S. Jul 03 '19 at 09:24
  • I think I see now - the interceptor is a singleton, so the observable is also a singleton. That means THE NEXT TIME a refresh occurs, that observable will still contain the access token from last time... which is an expired token! So you put null into the event stream to prevent that bug - not for this refresh cycle, but for the NEXT one! Does that make sense?? – lonix Jul 03 '19 at 11:42
  • 1
    The main reason is, that you often fire multiple requests parallel. The first one hits the refresh mechanism but you want the other requests to wait for a new token. They wait here: `return this.tokenSubject.pipe(filter(token => token != null)` until `this.tokenSubject.next(newToken)` is triggered. If you don't emit null then `filter(token => token != null)` would not stop other requests and all would use the old token from the last refresh. It is actually not a bug but a feature :-) – J. S. Jul 03 '19 at 12:10
  • I understood it differently - the other (parallel) requests wait because `take(1)` is waiting for the first non-null event (the token), and then completes the stream. The NEXT time a refresh happens (let's say in 20 minutes), the `BehaviorSubject` still contains that token from last time, which is now expired! So we put null at the end of the stream to prevent the next refresh cycle from completing immediately with the expired token... I think we are saying the same thing in different ways! But I'm new to rxjs and not an expert like you, so thanks for helping... you helped me a lot!! :-) – lonix Jul 03 '19 at 12:22
  • 1
    @J.S. When token refreshed, `next.hande(request)` is skipped. I can see in Dev Tools how my initial request got 401, then immediately token is refreshed, however, initial request is not called once again. How I can fix it? – Vlad Aug 03 '19 at 19:06
  • i have used the same functionality, and i am getting refreshToken and my previously failed request also resumes and got the data but somehow the angular does not render after getting updated data, what to do, please help, @J.S. – Haritsinh Gohil Jan 23 '20 at 13:44
  • 1
    @HaritsinhGohil it seems that it has something to do with your component rather than with the interceptor. Can you open a new question and post your code of the component? – J. S. Feb 04 '20 at 15:42
  • if anyone has posted a new quesion then please attach a link here. For me I am getting new refresh token, but some of the pending requests still use old token. – Code Name Jack Apr 07 '20 at 08:57
  • This is what I have been searching for sooo long... Thanks you so much for your awesome answer!! – Tom el Safadi Oct 09 '20 at 05:20
  • I keep coming back to this answer every time I need this functionality. This became like a reference to me! – Yazan Khalaileh May 01 '21 at 18:24
  • Where do you subscribe to the refreshToken or the BehaviorSubject to make the code execute? – bradrice May 03 '23 at 16:34
  • This a http interceptor angular does subscribe to it: https://angular.io/api/common/http/HttpInterceptor – J. S. May 10 '23 at 06:45
1

Below is the code for calling refresh token and after getting refresh token calls failed API's,

Comments in source code would help you to understand flow. Its tested and fine for below scenarios

1) If single request fails due to 401 then it will called for refresh token and will call failed API with updated token.

2) If multiple requests fails due to 401 then it will called for refresh token and will call failed API with updated token.

3) It will not call token API repeatedly

If still anyone found new scenario where this code not working please inform me so I will test and update it accordingly.

import { Injectable } from "@angular/core";
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse, HttpHeaders, HttpClient, HttpResponse } from "@angular/common/http";

import { Observable } from "rxjs/Observable";
import { throwError, BehaviorSubject } from 'rxjs';
import { catchError, switchMap, tap, filter, take, finalize } from 'rxjs/operators';
import { TOKENAPIURL } from 'src/environments/environment';
import { SessionService } from '../services/session.service';
import { AuthService } from '../services/auth.service';

/**
 * @author Pravin P Patil
 * @version 1.0
 * @description Interceptor for handling requests which giving 401 unauthorized and will call for 
 * refresh token and if token received successfully it will call failed (401) api again for maintaining the application momentum
 */
@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {

    private isRefreshing = false;
    private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);


    constructor(private http: HttpClient, private sessionService: SessionService, private authService: AuthService) { }

    /**
     * 
     * @param request HttpRequest
     * @param next HttpHandler
     * @description intercept method which calls every time before sending requst to server
     */
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // Taking an access token
        const accessToken = sessionStorage.getItem('ACCESS_TOKEN');
        // cloing a request and adding Authorization header with token
        request = this.addToken(request, accessToken);
        // sending request to server and checking for error with status 401 unauthorized
        return next.handle(request).pipe(
            catchError(error => {
                if (error instanceof HttpErrorResponse && error.status === 401) {
                    // calling refresh token api and if got success extracting token from response and calling failed api due to 401                    
                    return this.handle401Error(request, next);
                } // If api not throwing 401 but gives an error throwing error
                else {
                    return throwError(error);
                }
            }));
    }

    /**
     * 
     * @param request HttpRequest<any>
     * @param token token to in Authorization header
     */
    private addToken(request: HttpRequest<any>, token: string) {
        return request.clone({
            setHeaders: { 'Authorization': `Bearer ${token}` }
        });
    }

    /**
     * This method will called when any api fails due to 401 and calsl for refresh token
     */
    private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
        // If Refresh token api is not already in progress
        if (this.isRefreshing) {
            // 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(token => token != null),
                    take(1),
                    switchMap(jwt => {
                        return next.handle(this.addToken(request, jwt))
                    }));
        } else {
            // updating variable with api is in progress
            this.isRefreshing = true;
            // Set the refreshTokenSubject to null so that subsequent API calls will wait until the new token has been retrieved
            this.refreshTokenSubject.next(null);

            const refreshToken = sessionStorage.getItem('REFRESH_TOKEN');
            // Token String for Refresh token OWIN Authentication
            const tokenDataString = `refresh_token=${refreshToken}&grant_type=refresh_token`;
            const httpOptions = {
                headers: new HttpHeaders({
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'X-Skip-Interceptor': ''
                })
            };
            return this.http.post<any>(TOKENAPIURL, tokenDataString, httpOptions)
                .pipe(switchMap((tokens) => {
                    this.isRefreshing = false;
                    this.refreshTokenSubject.next(tokens.access_token);
                    // updating value of expires in variable                    
                    sessionStorage.setItem('ACCESS_TOKEN', tokens.access_token);
                    return next.handle(this.addToken(request, tokens.access_token));
                }));
        }
    }
}
Pravin P Patil
  • 169
  • 1
  • 5
  • can you please elaborate how it works? Specifically, at what point a new token is stored into LocalStorage? – Mark Nov 20 '19 at 19:52
  • 1
    I am adapting your code so it's not exactly the same bur same concept. I am running two request at the same time. Only first one is retried. second failed, but not retried.Any tips? – Mark Nov 20 '19 at 19:57
  • Hi Mark, you are right I have tested it again in different environment where it is failing for multiple API's. – Pravin P Patil Nov 26 '19 at 14:07
  • I am working on it, planning to store failed API except token API and after receiving token will retry for failed API. – Pravin P Patil Nov 26 '19 at 14:08
  • To answer a @Mark Question we can check for API failed due to 401 (Unauthorization) and will store those requests in array with next(HttpHandler) once token API done her task then we can call failed API with updated JWT. I hope this will help you and others. – Pravin P Patil Dec 18 '19 at 08:24
  • @PravinPPatil but in my case, the next handler is not called until it subscribes, but this is not happing in the interceptor function. can you please explain where is the error? if you getting me. – Muhammad Shahab Nov 29 '22 at 07:55
0

You can do something like this :

import { HttpErrorResponse } from '@angular/common/http';

return next.handle(req).pipe(
  catchError((err: any) => {
    if (err instanceof HttpErrorResponse && err.status 401) {
     return this._authenticationService.refresh()
       .pipe(tap(
         (success) => {},
         (err) => {
           this._authenticationService.logOut();
           throw error;
         }
       ).mergeMap((res) => {
         this._authenticationService.processLoginResponse(res);
         newReq.headers.set("Authorization", "Bearer " + this._authenticationService.authResponse.token)
         return next.handle(newReq)
       });
    } else {
      return Observable.of({});
    }
  }
));
Florian
  • 1,473
  • 10
  • 15
  • 1
    same problem, the second return next.handle is skipped –  Jan 23 '19 at 12:59
  • @Daniel I updated the answer, you tried to return a new observable in `subscribe` you should use `mergeMap/flatMap` instead. – Florian Jan 23 '19 at 13:17
  • @Daniel, so, you have a solution? Cause it doesn't really work for me. I can see that `switchMap`/`mergeMap`/`flatMap` refreshing token and then this token is added to the `request`, however it's not called, just skipped. – Vlad Aug 03 '19 at 19:01