119

Hi I am trying to figure out how implement the new angular interceptors and handle 401 unauthorized errors by refreshing the token and retrying the request. This is the guide I have been following: https://ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors

I am successfully caching the failed requests and can refresh the token but I cannot figure out how to resend the requests that previously failed. I also want to get this to work with the resolvers I am currently using.

token.interceptor.ts

return next.handle( request ).do(( event: HttpEvent<any> ) => {
        if ( event instanceof HttpResponse ) {
            // do stuff with response if you want
        }
    }, ( err: any ) => {
        if ( err instanceof HttpErrorResponse ) {
            if ( err.status === 401 ) {
                console.log( err );
                this.auth.collectFailedRequest( request );
                this.auth.refreshToken().subscribe( resp => {
                    if ( !resp ) {
                        console.log( "Invalid" );
                    } else {
                        this.auth.retryFailedRequests();
                    }
                } );

            }
        }
    } );

authentication.service.ts

cachedRequests: Array<HttpRequest<any>> = [];

public collectFailedRequest ( request ): void {
    this.cachedRequests.push( request );
}

public retryFailedRequests (): void {
    // retry the requests. this method can
    // be called after the token is refreshed
    this.cachedRequests.forEach( request => {
        request = request.clone( {
            setHeaders: {
                Accept: 'application/json',
                'Content-Type': 'application/json',
                Authorization: `Bearer ${ this.getToken() }`
            }
        } );
        //??What to do here
    } );
}

The above retryFailedRequests() file is what I can't figure out. How do I resend the requests and make them available to the route through the resolver after retrying?

This is all the relevant code if that helps: https://gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9

rmlarsen
  • 177
  • 1
  • 13
Kovaci
  • 1,211
  • 2
  • 9
  • 8

13 Answers13

141

My final solution. Works with parallel requests.

UPDATE: The code updated with Angular 9 / RxJS 6, error handling and fix looping when refreshToken fails

import { HttpRequest, HttpHandler, HttpInterceptor, HTTP_INTERCEPTORS } from "@angular/common/http";
import { Injector } from "@angular/core";
import { Router } from "@angular/router";
import { Subject, Observable, throwError } from "rxjs";
import { catchError, switchMap, tap} from "rxjs/operators";
import { AuthService } from "./auth.service";

export class AuthInterceptor implements HttpInterceptor {

    authService;
    refreshTokenInProgress = false;

    tokenRefreshedSource = new Subject();
    tokenRefreshed$ = this.tokenRefreshedSource.asObservable();

    constructor(private injector: Injector, private router: Router) {}

    addAuthHeader(request) {
        const authHeader = this.authService.getAuthorizationHeader();
        if (authHeader) {
            return request.clone({
                setHeaders: {
                    "Authorization": authHeader
                }
            });
        }
        return request;
    }

    refreshToken(): Observable<any> {
        if (this.refreshTokenInProgress) {
            return new Observable(observer => {
                this.tokenRefreshed$.subscribe(() => {
                    observer.next();
                    observer.complete();
                });
            });
        } else {
            this.refreshTokenInProgress = true;

            return this.authService.refreshToken().pipe(
                tap(() => {
                    this.refreshTokenInProgress = false;
                    this.tokenRefreshedSource.next();
                }),
                catchError(() => {
                    this.refreshTokenInProgress = false;
                    this.logout();
                }));
        }
    }

    logout() {
        this.authService.logout();
        this.router.navigate(["login"]);
    }

    handleResponseError(error, request?, next?) {
        // Business error
        if (error.status === 400) {
            // Show message
        }

        // Invalid token error
        else if (error.status === 401) {
            return this.refreshToken().pipe(
                switchMap(() => {
                    request = this.addAuthHeader(request);
                    return next.handle(request);
                }),
                catchError(e => {
                    if (e.status !== 401) {
                        return this.handleResponseError(e);
                    } else {
                        this.logout();
                    }
                }));
        }

        // Access denied error
        else if (error.status === 403) {
            // Show message
            // Logout
            this.logout();
        }

        // Server error
        else if (error.status === 500) {
            // Show message
        }

        // Maintenance error
        else if (error.status === 503) {
            // Show message
            // Redirect to the maintenance page
        }

        return throwError(error);
    }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
        this.authService = this.injector.get(AuthService);

        // Handle request
        request = this.addAuthHeader(request);

        // Handle response
        return next.handle(request).pipe(catchError(error => {
            return this.handleResponseError(error, request, next);
        }));
    }
}

export const AuthInterceptorProvider = {
    provide: HTTP_INTERCEPTORS,
    useClass: AuthInterceptor,
    multi: true
};
Andrei Astrouski
  • 1,410
  • 1
  • 9
  • 7
  • @Andrei Ostrovski I am trying to use the same solution but the `.flatMap` is never triggered. I tried replacing that with a subscribe and that works fine but then that breaks the observable stream as I need to return an observable. How can I deal with that? – jerry Nov 13 '17 at 06:58
  • 1
    @jerry Check that you use CATCH handler, not DO – Andrei Astrouski Nov 14 '17 at 10:04
  • 3
    @AndreiOstrovski, could you please update the answer with `imports` and the code of the AuthService? – takeshin Dec 07 '17 at 19:36
  • 6
    I have a feeling that if for some reason this.authService.refreshToken() fails, all parallel queries waiting for refresh will wait forever. – Maksim Gumerov Dec 18 '17 at 12:22
  • @AndreiOstrovski tks for u comment, its very import check if your not use (do => catch) i lost very time. tks. – Renan Degrandi Feb 23 '18 at 14:48
  • 2
    The catch on the refresh token never calls for me. It hit the Observable .throw. – jamesmpw Mar 02 '18 at 18:53
  • @takeshin The AuthService is a class reference for the injector. Here's a placeholder import { Injectable } from '@angular/core'; @Injectable() export class AuthService {refreshToken() { /*logic*/ }getAuthorizationHeader() { /*logic*/ } } – Jessy May 22 '18 at 11:58
  • 1
    Good job! Work like a charm! – Valery Lyatsevich Jun 07 '18 at 15:09
  • How to write a spec for the AuthInterceptor? Especially the double `next.handle` logic with `HttpTestingController` and `expectOne` routines. Thanks – Felix Jun 26 '18 at 10:46
  • Passing modified request to `next.handle` made my day. Got fully covered interception/refresh/retry scenario in unit tests. – Antoniossss Aug 02 '18 at 10:44
  • @Antoniossss You have unit tests for this ? :-) You'd like to enlighten us with an example ? – Stephane Sep 04 '18 at 17:00
  • 1
    Im out of that project already. What seems to be the problem? https://angular.io/guide/http#testing-http-requests – Antoniossss Sep 04 '18 at 18:01
  • Like testing the retry.. I can't even see how to go about this. I'm starting wth Angular and haven't tried writting a unit test yet. – Stephane Sep 07 '18 at 15:29
  • million articles read, only this one worked for me i wish i could vote up thousand times – Wasif Khalil Feb 06 '19 at 09:26
  • 1
    Is not supporting parallel requests. If a refresh token is being processed and a new request is made, the refresh token request is being cancelled (but processed in the server side anyway). The idea is to put any request in a queue, and once the token is refreshed, then make all the queued requests. – Martín Mar 21 '19 at 14:24
  • 2
    Guys, it works with parallel and sequential requests. You send 5 requests, they return 401, then 1 refreshToken is performed, and 5 requests again. If your 5 requests are sequential, after first 401 we send refreshToken, then the first request again and other 4 requests. – Andrei Astrouski Jun 28 '19 at 10:38
  • Is there there something how this could also work with redirecting to login prompt? I am not working with refresh tokens, but rather want to redirect them to type in their password again. The problem is, that this behaviour can happen when filling out a form, so after logging in and resendig the request, the form wont display errors. – Seba M Mar 08 '20 at 18:37
  • I think this is missing a very important piece... .catch(() => has to set `this.refreshTokenInProgress = false;` Without it, any errors cause the subsequent 401 to go into an permanent queue because it thinks it's already in progress. – MDave May 09 '20 at 04:41
  • 2
    Why are you manually injecting a service when Angular could do that for you if you were decorate it with `@Injectable()` ? Also one catchError doesn't return anything. At least return `EMPTY`. – Győri Sándor May 15 '20 at 13:05
  • 2
    Lets say two requests fail. One calls the refreshtoken, the other sees that refresh is in progress and waits here tokenRefreshed$.subscribe. If the first request to refresh fails, won't the second be subscribed to this.tokenRefreshed$ forever, causing a memory leak? – Chano May 19 '21 at 23:53
  • 1
    Is a complete project with all code in a github repo anywhere? – Mr Smith Jan 20 '23 at 15:10
20

With the latest version of Angular (7.0.0) and rxjs (6.3.3), this is how I created a fully functional Auto Session recovery interceptor ensuring, if concurrent requests fail with 401, then also, it should only hit token refresh API once and pipe the failed requests to the response of that using switchMap and Subject. Below is how my interceptor code looks like. I have omitted the code for my auth service and store service as they are pretty standard service classes.

import {
  HttpErrorResponse,
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, Subject, throwError } from "rxjs";
import { catchError, switchMap } from "rxjs/operators";

import { AuthService } from "../auth/auth.service";
import { STATUS_CODE } from "../error-code";
import { UserSessionStoreService as StoreService } from "../store/user-session-store.service";

@Injectable()
export class SessionRecoveryInterceptor implements HttpInterceptor {
  constructor(
    private readonly store: StoreService,
    private readonly sessionService: AuthService
  ) {}

  private _refreshSubject: Subject<any> = new Subject<any>();

  private _ifTokenExpired() {
    this._refreshSubject.subscribe({
      complete: () => {
        this._refreshSubject = new Subject<any>();
      }
    });
    if (this._refreshSubject.observers.length === 1) {
      this.sessionService.refreshToken().subscribe(this._refreshSubject);
    }
    return this._refreshSubject;
  }

  private _checkTokenExpiryErr(error: HttpErrorResponse): boolean {
    return (
      error.status &&
      error.status === STATUS_CODE.UNAUTHORIZED &&
      error.error.message === "TokenExpired"
    );
  }

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    if (req.url.endsWith("/logout") || req.url.endsWith("/token-refresh")) {
      return next.handle(req);
    } else {
      return next.handle(req).pipe(
        catchError((error, caught) => {
          if (error instanceof HttpErrorResponse) {
            if (this._checkTokenExpiryErr(error)) {
              return this._ifTokenExpired().pipe(
                switchMap(() => {
                  return next.handle(this.updateHeader(req));
                })
              );
            } else {
              return throwError(error);
            }
          }
          return caught;
        })
      );
    }
  }

  updateHeader(req) {
    const authToken = this.store.getAccessToken();
    req = req.clone({
      headers: req.headers.set("Authorization", `Bearer ${authToken}`)
    });
    return req;
  }
}

As per @anton-toshik comment, I thought it's a good idea to explain the functioning of this code in a write-up. You can have a read at my article here for the explanation and understanding of this code (how and why it works?). Hope it helps.

Samarpan
  • 913
  • 5
  • 12
  • 1
    good work, the second `return` inside the `intercept` function should look like this: `return next.handle(this.updateHeader(req)).pipe(`. Currently you only send the auth token after refreshing it... – malimo Jan 27 '19 at 09:08
  • I think I am doing that via switchmap. Please check again. Let me know if I misunderstood your point. – Samarpan Jan 29 '19 at 07:19
  • yes it basically works but you always send the request twice - once without the header, and then after it failed with the header.... – malimo Feb 01 '19 at 13:25
  • @SamarpanBhattacharya This works. I think this answer could do with an explanation with semantics for someone like me who doesn't understand how Observable's work. – Anton Toshik Feb 03 '19 at 17:11
  • may I know this function code this.sessionService.refreshToken() ? thanks – Nika Kurashvili Feb 09 '19 at 17:55
  • @AntonToshik, updated answer with a link to my article detailing the code flow. Hope it helps. – Samarpan Apr 08 '19 at 06:00
  • Blog article is very good. But could you please explain how subsequent requests are queued so that they are tried after the refresh is performed? Where is the wait/queuing performed? If 5 requests come in, the first triggers a refresh... what happens to the other 4? How are they queued? – lonix Jun 27 '19 at 11:53
  • return this._ifTokenExpired().pipe( switchMap(() => { return next.handle(this.updateHeader(req)); }) ); This takes care of that. switchMap switches the refresh token hit with original request held earlier using next.handle – Samarpan Jun 28 '19 at 12:17
  • 1
    @NikaKurashvili, This method definition worked for me: `public refreshToken(){const url:string=environment.apiUrl+API_ENDPOINTS.REFRESH_TOKEN;const req:any={token:this.getAuthToken()};const head={};const header={headers:newHttpHeaders(head)};return this.http.post(url,req,header).pipe(map(resp=>{const actualToken:string=resp['data'];if(actualToken){this.setLocalStorage('authToken',actualToken);}return resp;}));}` – Shrinivas Jul 16 '19 at 06:56
  • @Samarpan thanks a ton for the awesome explanation. – Neeraj Jain Feb 27 '20 at 09:45
15

I had to solve the following requirements:

  • ✅ Refresh token only once for multiple requests
  • ✅ Log out user if refreshToken failed
  • ✅ Log out if user gets an error after first refreshing
  • ✅ Queue all requests while token is being refreshed

As a result I've collected different options in order to refresh token in Angular:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    let retries = 0;
    return this.authService.token$.pipe(
      map(token => req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })),
      concatMap(authReq => next.handle(authReq)),
      // Catch the 401 and handle it by refreshing the token and restarting the chain
      // (where a new subscription to this.auth.token will get the latest token).
      catchError((err, restart) => {
        // If the request is unauthorized, try refreshing the token before restarting.
        if (err.status === 401 && retries === 0) {
          retries++;
    
          return concat(this.authService.refreshToken$, restart);
        }
    
        if (retries > 0) {
          this.authService.logout();
        }
    
        return throwError(err);
      })
    );
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return this.authService.token$.pipe(
      map(token => req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })),
      concatMap(authReq => next.handle(authReq)),
      retryWhen((errors: Observable<any>) => errors.pipe(
        mergeMap((error, index) => {
          // any other error than 401 with {error: 'invalid_grant'} should be ignored by this retryWhen
          if (error.status !== 401) {
            return throwError(error);
          }
    
          if (index === 0) {
            // first time execute refresh token logic...
            return this.authService.refreshToken$;
          }
    
          this.authService.logout();
          return throwError(error);
        }),
        take(2)
        // first request should refresh token and retry,
        // if there's still an error the second time is the last time and should navigate to login
      )),
    );
}

All these options are horoughly tested and can be found in angular-refresh-token github repo

See also:

yurzui
  • 205,937
  • 32
  • 433
  • 399
11

Andrei Ostrovski's final solution works really well, but does not work if the refresh token is also expired (assuming you're making an api call to refresh). After some digging, I realised that the refresh token API call was also intercepted by the interceptor. I've had to add an if statement to handle this.

 intercept( request: HttpRequest<any>, next: HttpHandler ):Observable<any> {
   this.authService = this.injector.get( AuthenticationService );
   request = this.addAuthHeader(request);

   return next.handle( request ).catch( error => {
     if ( error.status === 401 ) {

     // The refreshToken api failure is also caught so we need to handle it here
       if (error.url === environment.api_url + '/refresh') {
         this.refreshTokenHasFailed = true;
         this.authService.logout();
         return Observable.throw( error );
       }

       return this.refreshAccessToken()
         .switchMap( () => {
           request = this.addAuthHeader( request );
           return next.handle( request );
         })
         .catch((err) => {
           this.refreshTokenHasFailed = true;
           this.authService.logout();
           return Observable.throw( err );
         });
     }

     return Observable.throw( error );
   });
 }
James Lieu
  • 176
  • 2
  • 6
  • Could you show where else you play with the `refreshTokenHasFailed` member boolean ? – Stephane Sep 04 '18 at 17:25
  • 1
    You can find it on Andrei Ostrovski's solution above, I've basically used that but added the if statement to handle when the refresh endpoint is intercepted. – James Lieu Oct 11 '18 at 09:15
  • This doesn't make sense, why would the refresh return a 401? The point is that it's calling the refresh after authentication failed, so your refresh API shouldn't be authenticating at all, and shouldn't be returning a 401. – MDave May 09 '20 at 04:08
  • 2
    Refresh tokens can have expiry dates. In our use case, it was set to expire after 4 hours, if the user were to close their browser at the end of the day and return the following morning, the refresh token would have expired by that point and therefore we required them to log back in again. If your refresh token didn't expire then of course would you wouldn't need to apply this logic – James Lieu May 10 '20 at 09:37
9

I ran into a similar problem as well and I think the collect/retry logic is overly complicated. Instead, we can just use the catch operator to check for the 401, then watch for the token refresh, and rerun the request:

return next.handle(this.applyCredentials(req))
  .catch((error, caught) => {
    if (!this.isAuthError(error)) {
      throw error;
    }
    return this.auth.refreshToken().first().flatMap((resp) => {
      if (!resp) {
        throw error;
      }
      return next.handle(this.applyCredentials(req));
    });
  }) as any;

...

private isAuthError(error: any): boolean {
  return error instanceof HttpErrorResponse && error.status === 401;
}
rdukeshier
  • 99
  • 4
  • 1
    I like to use a custom status code of 498 to identify an expired token versus 401 which can also indicated not enough priv – Joseph Carroll Sep 25 '17 at 15:59
  • 1
    Hi, am trying using return next.handle(reqClode) and does nothing, my code is different from your abit but the part not working is return part. authService.createToken(authToken, refreshToken); this.inflightAuthRequest = null; return next.handle(req.clone({ headers: req.headers.set(appGlobals.AUTH_TOKEN_KEY, authToken) })); –  Sep 28 '17 at 00:05
  • 7
    The collect/retry logic is not overly complicated, is the way you have to do it if you don't want to make multiple requests to the refreshToken endpoint while your token is expired. Say your token expired, and you make 5 requests at almost the same time. With the logic in this comment, 5 new refresh token will be generated server side. – Marius Lazar Oct 13 '17 at 15:09
  • 4
    @JosephCarroll usually not enough privileges is 403 – andrea.spot. Feb 28 '18 at 15:17
5

Based on this example, here's my piece

@Injectable({
    providedIn: 'root'
})
export class AuthInterceptor implements HttpInterceptor {

    constructor(private loginService: LoginService) { }

    /**
     * Intercept request to authorize request with oauth service.
     * @param req original request
     * @param next next
     */
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
        const self = this;

        if (self.checkUrl(req)) {
            // Authorization handler observable
            const authHandle = defer(() => {
                // Add authorization to request
                const authorizedReq = req.clone({
                    headers: req.headers.set('Authorization', self.loginService.getAccessToken()
                });
                // Execute
                return next.handle(authorizedReq);
            });

            return authHandle.pipe(
                catchError((requestError, retryRequest) => {
                    if (requestError instanceof HttpErrorResponse && requestError.status === 401) {
                        if (self.loginService.isRememberMe()) {
                            // Authrozation failed, retry if user have `refresh_token` (remember me).
                            return from(self.loginService.refreshToken()).pipe(
                                catchError((refreshTokenError) => {
                                    // Refresh token failed, logout
                                    self.loginService.invalidateSession();
                                    // Emit UserSessionExpiredError
                                    return throwError(new UserSessionExpiredError('refresh_token failed'));
                                }),
                                mergeMap(() => retryRequest)
                            );
                        } else {
                            // Access token failed, logout
                            self.loginService.invalidateSession();
                            // Emit UserSessionExpiredError
                            return throwError(new UserSessionExpiredError('refresh_token failed')); 
                        }
                    } else {
                        // Re-throw response error
                        return throwError(requestError);
                    }
                })
            );
        } else {
            return next.handle(req);
        }
    }

    /**
     * Check if request is required authentication.
     * @param req request
     */
    private checkUrl(req: HttpRequest<any>) {
        // Your logic to check if the request need authorization.
        return true;
    }
}

You may want to check if user enabled Remember Me to use refresh token for retrying or just redirect to logout page.

Fyi, the LoginService has the following methods:
- getAccessToken(): string - return the current access_token
- isRememberMe(): boolean - check if user have refresh_token
- refreshToken(): Observable / Promise - Request to oauth server for new access_token using refresh_token
- invalidateSession(): void - remove all user info and redirect to logout page

Thanh Nhan
  • 453
  • 6
  • 17
  • Do you have an issue with multiple requests sending multiple refresh requests? – CodingGorilla Jan 19 '18 at 19:50
  • This version I like the most but I'm having an issue where mine makes a request, when that returns 401 is tries to refresh, when that returns error it continually tries ti send the request again, never stopping. Am I doing something wrongs? – jamesmpw Mar 03 '18 at 03:00
  • Sorry, the one before I did not tested carefully. Just edited the my post with the tested one I'm using (also migrate to rxjs6 and refesh token, check url). – Thanh Nhan Jul 03 '18 at 00:23
1

Ideally, you want to check isTokenExpired before request sent. And if expired refresh the token and add refreshed in the header.

Other than that retry operator may help with your logic of refreshing token on 401 response.

Use the RxJS retry operator in your service where you are making a request. It accepts a retryCount argument. If not provided, it will retry the sequence indefinitely.

In your interceptor on response refresh the token and return the error. When your service gets back the error but now retry operator is being used so it will retry the request and this time with the refreshed token(Interceptor uses refreshed token to add in the header.)

import {HttpClient} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Rx';

@Injectable()
export class YourService {

  constructor(private http: HttpClient) {}

  search(params: any) {
    let tryCount = 0;
    return this.http.post('https://abcdYourApiUrl.com/search', params)
      .retry(2);
  }
}
Lahar Shah
  • 7,032
  • 4
  • 31
  • 39
1

On the most accepted answer by Andrei Ostrovski, people comment about memory leak when token refresh request fails for some reason. One could mitigate this by using RxJS timeout operator, like this:

//...

 tokenRefreshTimeout = 60000;

//...

    // Invalid token error
            else if (error.status === 401) {
                return this.refreshToken().pipe(
                    timeout(this.tokenRefreshTimeout), //added timeout here
                    switchMap(() => {
                        request = this.addAuthHeader(request);
                        return next.handle(request);
                    }),
//...

(sorry, I don't have enough rep to comment, also I cannot suggest an edit because the edit queue is always full)

David Mališ
  • 196
  • 4
0
To support ES6 syntax the solution needs to be bit modify and that is as following also included te loader handler on multiple request


        private refreshTokenInProgress = false;
        private activeRequests = 0;
        private tokenRefreshedSource = new Subject();
        private tokenRefreshed$ = this.tokenRefreshedSource.asObservable();
        private subscribedObservable$: Subscription = new Subscription();



 intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (this.activeRequests === 0) {
            this.loaderService.loadLoader.next(true);
        }
        this.activeRequests++;

        // Handle request
        request = this.addAuthHeader(request);

        // NOTE: if the flag is true it will execute retry auth token mechanism ie. by using refresh token it will fetch new auth token and will retry failed api with new token
        if (environment.retryAuthTokenMechanism) {
            // Handle response
            return next.handle(request).pipe(
                catchError(error => {
                    if (this.authenticationService.refreshShouldHappen(error)) {
                        return this.refreshToken().pipe(
                            switchMap(() => {
                                request = this.addAuthHeader(request);
                                return next.handle(request);
                            }),
                            catchError(() => {
                                this.authenticationService.setInterruptedUrl(this.router.url);
                                this.logout();
                                return EMPTY;
                            })
                        );
                    }

                    return EMPTY;
                }),
                finalize(() => {
                    this.hideLoader();
                })
            );
        } else {
            return next.handle(request).pipe(
                catchError(() => {
                    this.logout();
                    return EMPTY;
                }),
                finalize(() => {
                    this.hideLoader();
                })
            );
        }
    }

    ngOnDestroy(): void {
        this.subscribedObservable$.unsubscribe();
    }

    /**
     * @description Hides loader when all request gets complete
     */
    private hideLoader() {
        this.activeRequests--;
        if (this.activeRequests === 0) {
            this.loaderService.loadLoader.next(false);
        }
    }

    /**
     * @description set new auth token by existing refresh token
     */
    private refreshToken() {
        if (this.refreshTokenInProgress) {
            return new Observable(observer => {
                this.subscribedObservable$.add(
                    this.tokenRefreshed$.subscribe(() => {
                        observer.next();
                        observer.complete();
                    })
                );
            });
        } else {
            this.refreshTokenInProgress = true;

            return this.authenticationService.getNewAccessTokenByRefreshToken().pipe(tap(newAuthToken => {
            this.authenticationService.updateAccessToken(newAuthToken.access_token);
            this.refreshTokenInProgress = false;
            this.tokenRefreshedSource.next();
        }));
        }
    }

    private addAuthHeader(request: HttpRequest<any>) {
        const accessToken = this.authenticationService.getAccessTokenOnly();
        return request.clone({
            setHeaders: {
                Authorization: `Bearer ${accessToken}`
            }
        });
    }

    /**
     * @todo move in common service or auth service once tested
     * logout and redirect to login
     */
    private logout() {
        this.authenticationService.removeSavedUserDetailsAndLogout();
    }
Saurabh Deshmukh
  • 163
  • 1
  • 15
0

My Answer

In this case just handler 401

@Injectable()
export class AuthHttpInterceptor implements HttpInterceptor {

  logoutUser$ = defer(() => (this.authService.logout(), EMPTY));
  refresh$ = defer(() => this.authService.refreshTokenFromServer()).pipe(catchError(() => this.logoutUser$), share());

  constructor(private authService: AuthService) { }

  private applyCredentials(request: HttpRequest<any>): HttpRequest<any> {
    return request.clone({
      setHeaders: { Authorization: 'Bearer ' + this.authService.accessToken }
    });
  }

  public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (InterceptorSkipHeader.checkHeader(request)) {
      const req = InterceptorSkipHeader.deleteHeader(request);
      return next.handle(req);
    }
    const nextHandle$ = defer(() => next.handle(this.applyCredentials(request)));
    return iif(() => this.authService.tokenIsEmpty, this.logoutUser$, nextHandle$).pipe(this.httpErrorsHandler());
  }

  httpErrorsHandler() {
    return (source$: Observable<any>) => source$.pipe(
      catch401Error(() => this.handle401Error(source$)),
      catch400Error((err) => EMPTY),
      catch403Error((err) => EMPTY),
      catch406Error((err) => EMPTY),
      catch500Error((err) => EMPTY),
    );
  }

  handle401Error(retry$: Observable<any>): Observable<any> {
    return retry$.pipe(
      startWhen(this.refresh$),
      takeUntil(this.authService.logout$),
      catch401Error(() => this.logoutUser$),
    );
  }
}

full code ( auth-http-interceptor.ts )

step 1, Create two Observable

logoutUser$ :

  • use defer() do your logout logic (like clear token from LocalStorage) and retun EMPTY

refresh$ :

  • use defer create refresh$ Observable, make it always take new refresh token to call refresh API

  • logout on catch error

  • share() this Observable(make all 401 wait same refresh API back)

logoutUser$ = defer(() => (this.authService.logout(), EMPTY));
refresh$ = defer(() => this.authService.refreshTokenFromServer()).pipe(catchError(() => this.logoutUser$), share());

step 2, Skip interceptor

just make api skip interceptor ( uitls.ts )

class Xheader {
  static readonly interceptorSkipHeader = new Xheader('interceptorSkipHeader');

  readonly headers = { [this.headerName]: this.headerName };
  readonly options = { headers: this.headers };

  private constructor(readonly headerName: string) { }

  public checkHeader({ headers }: HttpRequest<any>) {
    return headers.has(this.headerName);
  }

  public deleteHeader(request: HttpRequest<any>) {
    return request.clone({ headers: request.headers.delete(this.headerName) });
  }
}

export const InterceptorSkipHeader = Xheader.interceptorSkipHeader;

like this InterceptorSkipHeader.options ( auth.service.ts)

refreshTokenFromServer(): Observable<Token> {
    return this.http.post<Token>(this.authApi + '/refreshToken', this.token, InterceptorSkipHeader.options).pipe(setTokenToLocalStorage());
}

step 3, Interceptor

4

Has skip header InterceptorSkipHeader.checkHeader(request)

  • delete and return without handler

Else, handler

  1. create nextHandle$ with access token : applyCredentials(request) use defer() ( always take new access token )
  2. use iif() check if token is empty will logoutUser$, else nextHandle$
  3. add httpErrorsHandler() operator, handler this stream
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (InterceptorSkipHeader.checkHeader(request)) {
      const req = InterceptorSkipHeader.deleteHeader(request);
      return next.handle(req);
    }
    const nextHandle$ = defer(() => next.handle(this.applyCredentials(request)));
    return iif(() => this.authService.tokenIsEmpty, this.logoutUser$, nextHandle$).pipe(this.httpErrorsHandler());
  }

Add access token function

private applyCredentials(request: HttpRequest<any>): HttpRequest<any> {
    return request.clone({
      setHeaders: { Authorization: 'Bearer ' + this.authService.accessToken }
    });
  }

step 4, Custom operator

We should create some custom operator before error Handler

catchHttpError operator

In this case we just handler 401

  • catch401Error : catch http 401
  • catch400Error : catch http 400
  • catch403Error : catch http 403
  • catch406Error : catch http 406
  • catch500Error : catch http 500
function catchHttpError(...status: Array<number>) {
  const statusMap = status.reduce((m, v) => m.set(v, v), new Map());
  return (next: (err: HttpErrorResponse) => Observable<any>) => {
    return catchError((err) => err instanceof HttpErrorResponse && statusMap.has(err.status) ? next(err) : throwError(err));
  };
}

const catch401Error = catchHttpError(401);
const catch400Error = catchHttpError(400);
const catch403Error = catchHttpError(403);
const catch406Error = catchHttpError(406);
const catch500Error = catchHttpError(500);

startWhen operator (uitls.ts)

equal delayWhen() second parameter (subscriptionDelay)

export function startWhen<T>(subscriptionDelay: Observable<any>) {
  return (source$: Observable<T>) => concat(subscriptionDelay.pipe(take(1), ignoreElements()), source$);
}

step 5, Http error handler

5

In this case we just handler 401

catch401Error must be the first (make sure other error handler will catch retry API error)

  • handle401Error(source$) will retry source$ (previous Observable)
httpErrorsHandler() {
  return (source$: Observable<any>) => source$.pipe(
    catch401Error(() => this.handle401Error(source$)),
    catch400Error((err) => EMPTY),
    catch403Error((err) => EMPTY),
    catch406Error((err) => EMPTY),
    catch500Error((err) => EMPTY),
  );
}

handle401Error

  • startWhen() : retry$ will wait refresh$ complete than call retry API
  • In process, if authService.logout$ trigger will stop stream (unsubscribe)
  • If retry API still 401 error will logout user
handle401Error(retry$: Observable<any>): Observable<any> {
  return retry$.pipe(
    startWhen(this.refresh$),
    takeUntil(this.authService.logout$),
    catch401Error(() => this.logoutUser$),
  );
}

https://medium.com/@eddylin1937/angular-interceptor-with-rxjs-refresh-token-176326c84a36

Eddy Lin
  • 513
  • 2
  • 6
-1

After api failed with HTTP Error 401 ,token-refresh api got called , all your failed and cached request can be retried using http interceptor.

if (this.isRefreshingToken && !req.url.endsWith(tokenURL)) {
      // check if unique url to be added in cachedRequest

      if (urlPresentIndex == -1) {
        this.cachedRequests.push(req);
        return this.tokenSubject.pipe(
          switchMap(() => next.handle(req)),
          tap((v) => {
            // delete request from catchedRequest if api gets called

            this.cachedRequests.splice(
              this.cachedRequests.findIndex(
                (httpRequest) => httpRequest.url == req.url
              ),
              1
            );
            return EMPTY;
          })
        );
      } else {
        //already in cached request array

        return EMPTY;
      }
    }

For more details you can read my medium article Token-Refresh-Interceptor-retry-failed-Requests

Check it out, how it works stackblitz

-3

I got this creating a new request based on the url of the failed request and sending the same body of the failed request.

 retryFailedRequests() {

this.auth.cachedRequests.forEach(request => {

  // get failed request body
  var payload = (request as any).payload;

  if (request.method == "POST") {
    this.service.post(request.url, payload).subscribe(
      then => {
        // request ok
      },
      error => {
        // error
      });

  }
  else if (request.method == "PUT") {

    this.service.put(request.url, payload).subscribe(
      then => {
       // request ok
      },
      error => {
        // error
      });
  }

  else if (request.method == "DELETE")

    this.service.delete(request.url, payload).subscribe(
      then => {
        // request ok
      },
      error => {
        // error
      });
});

this.auth.clearFailedRequests();        

}

-4

In your authentication.service.ts, you should have a HttpClient injected as a dependency

constructor(private http: HttpClient) { }

You can then re-submit the request (inside retryFailedRequests) as follow:

this.http.request(request).subscribe((response) => {
    // You need to subscribe to observer in order to "retry" your request
});