javascriptangularfirebasepromiseangular-http-interceptors

Reauthenticating Firebase User Token with Angular and HTTP Interceptor


I am running into a strange issue when using error catching within my Angular HTTP Interceptor code.

It seems that code in my chain of ".then()" statements is triggering out of order somehow.

Here's my code...

import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HTTP_INTERCEPTORS, HttpClient, HttpErrorResponse } from '@angular/common/http';
import { catchError, Observable, retry, switchMap, throwError } from 'rxjs';
import { FirebaseService } from '../services/firebase.service';
import { getAuth } from 'firebase/auth';

@Injectable()
export class HttpRequestInterceptor implements HttpInterceptor {

  //AW-Note: Adding this in here to try the appearance of "Persistent Login"
  static accessToken = '';
  static apiKey = 'API_KEY_HERE';

  constructor(private firebaseService: FirebaseService, private http: HttpClient) { }
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = localStorage.getItem('token');

    function refreshToken(): Promise<string> {
      // api call returns Promise<string>
      const auth = getAuth();
      const myUser = auth.currentUser;
      return myUser!.getIdToken(true);
    }

    async function getNewToken(): Promise<string> {
      const value = await refreshToken() // how to unwrap the value inside this  promise
      return value;
    }



    console.log('Flowing through HTTP Interceptor');

    req = req.clone({
      //If you comment out the below line you will no longer send Credentials with the request  
      withCredentials: true,
      setHeaders: { Authorization: `Bearer ${token}` }
    });

    return next.handle(req).pipe(/*retry(10),*/ catchError((err: HttpErrorResponse) => {
      console.log("ERROR FOUND! CHECKING TO SEE IF IT IS A 401 ERROR NEXT. Error: " + err);
      if (err.status === 401 || err.status === 0) {
        console.log("NEW TOKEN NEEDED - Yo we got that 401 error going on - let's try and refresh that thang");

        //Refresh Token from Firebase


        console.log("Old Token: " + JSON.stringify(token));



        console.log("Attempting to refresh token now...");

        getNewToken()
          .then((newToken) => {
            console.log('New Token: ' + JSON.stringify(newToken));
            localStorage.setItem('token', newToken);
            console.log('Updated Local Storage with new token');
          })
          .then(() => {
            console.log("Here is where the request will refresh");
            return next.handle(req.clone({
              withCredentials: true,
              setHeaders: {
                Authorization: `Bearer ${localStorage.getItem('token')}`
              }
            }))
          })
          .catch((error) => {
            console.log("Error in Getting new token... Error: " + JSON.stringify(error));
          });
      }
      return throwError(() => err);
    }))
  }
}


export const httpInterceptorProviders = [
  { provide: HTTP_INTERCEPTORS, useClass: HttpRequestInterceptor, multi: true },
];

So the code is successfully detecting an error once the token in localStorage has expired, and it is caught within the first "pipe()" block.

It successfully obtains a new token from Firebase but when it moves down to the final "then" statements to retry the API call with the new token, it seems like the API is called immediately after the console log statement "Attempting to refresh token now..." NOT "Here is where the request will refresh" as I would expect it to.

Here is the output from the console so you can see what I'm talking about...

Flowing through HTTP Interceptor

http.interceptor.ts:35
ERROR FOUND! CHECKING TO SEE IF IT IS A 401 ERROR NEXT. Error: [object Object]

http.interceptor.ts:44
NEW TOKEN NEEDED - Yo we got that 401 error going on - let's try and refresh that thang

http.interceptor.ts:46
Old Token: "eyJhbGciOiJSUzI1NiIsImtpZCI6ImU3OTMwMjdkYWI0YzcwNmQ2ODg0NGI4MDk2ZTBlYzQzMjYyMjIwMDAiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vdHV0b3JpYWwtYXBwLTEtODEyNDkiLCJhdWQiOiJ0dXRvcmlhbC1hcHAtMS04MTI0OSIsImF1dGhfdGltZSI6MTY4MjY4NTk3NCwidXNlcl9pZCI6ImUwUXdEYmZhVlpSdWZkaGF6S1FjdDU5ajdQVTIiLCJzdWIiOiJlMFF3RGJmYVZaUnVmZGhhektRY3Q1OWo3UFUyIiwiaWF0IjoxNjgyNzkwNjk4LCJleHAiOjE2ODI3OTQyOTgsImVtYWlsIjoiYWxleHdyaWdodDkyM0BnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsiYWxleHdyaWdodDkyM0BnbWFpbC5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9fQ.eaPLxmi-wtsDV941M7A0ouMJhraDhKVbd_EFmdnFH3HZSCHRK4K4HRFZZ6RrvRo0FOd60NZLstE-MXzXumb7N6J6LOWqE_pUrtsylqDE15Wp4hJmcHP8HZmJA4wve3JlLox_i3bNgzY7F4NTuGsij92nZd1qa-uDd6_SI4o2ABtZGGfX-11RAUKN7sjGRTy_zMM2ielb8im7W8q8KbdW1OxISqJDE4rbqyBlLBbUZWI9z1RMRpxD_s6k6y3bZfYTq_seFWaKLH3rEMfMGujS4x_ytlHqWaLbU7v3BXRxhZJsgui3r1ktQROFZdJwApybQ8-tpeEZwp0KDdnx2cautA"

http.interceptor.ts:51
Attempting to refresh token now...

HttpErrorResponse {headers: HttpHeaders, status: 0, statusText: 'Unknown Error', url: 'http://localhost:8080/api/tutorials', ok: false, …}

add-tutorial.component.ts:59
New Token: "eyJhbGciOiJSUzI1NiIsImtpZCI6ImU3OTMwMjdkYWI0YzcwNmQ2ODg0NGI4MDk2ZTBlYzQzMjYyMjIwMDAiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vdHV0b3JpYWwtYXBwLTEtODEyNDkiLCJhdWQiOiJ0dXRvcmlhbC1hcHAtMS04MTI0OSIsImF1dGhfdGltZSI6MTY4MjY4NTk3NCwidXNlcl9pZCI6ImUwUXdEYmZhVlpSdWZkaGF6S1FjdDU5ajdQVTIiLCJzdWIiOiJlMFF3RGJmYVZaUnVmZGhhektRY3Q1OWo3UFUyIiwiaWF0IjoxNjgyODc3MjIwLCJleHAiOjE2ODI4ODA4MjAsImVtYWlsIjoiYWxleHdyaWdodDkyM0BnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsiYWxleHdyaWdodDkyM0BnbWFpbC5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9fQ.OoykyUgJdb6ugVaiJClcQjV6DpKQAQCl2Jx1VwHSrS77NzN5F0bfRCTHAS-wvXYgbzhAGw39fwq67HXoC74OdugKbJpnhpyf3e-TWLdN1SD195aBedPvB6KtApD7nj4WPTlYuE8mupcqXB2mw8NvDuxyZLV4pOv4A7ou5J7mJkrwiGU0WboM5HPz7LeoXP5QlxpPZ8dQ4f2cnJ3AKD9SFSgrojhi1tgrOh80YCsq5ZP46VXPj4ym-MchQ4T4rQh6wjZ9NllzngM0-QvZD4Bz9m52g5hBaICqVQnVpbzX_xMnixZYI7N9LGC3F-GXEeLF-IhLgdEbcM8vL7LypOZyPQ"

http.interceptor.ts:59
Updated Local Storage with new token


http.interceptor.ts:61
About to retry the API Call now

http.interceptor.ts:65
Here is where the request will refresh

Any help here would be GREATLY appreciated. Am I doing something wrong with my .then() statements? Fairly new to typescript, angular and promise handling like this. THANKS.

I've tried to mess with the statements around the .then() statements but I think it is wierd that the return statement just seems to execute early

(the HTTP Request is made where the log says "HttpErrorResponse")

Thanks In Advance!


Solution

  • This was the Solution that ended up working for me.

    The main 2 changes are:

    1. Use from(refreshToken()) to convert the promise returned by refreshToken() into an observable that can be piped into the new switchMap operator.

    2. Move the retry logic into the switchMap operator, so that it retries the API call only after the new token has been obtained and added to local storage.

    import { Injectable } from '@angular/core';
    import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HTTP_INTERCEPTORS, HttpClient, HttpErrorResponse } from '@angular/common/http';
    import { catchError, Observable, retry, switchMap, throwError } from 'rxjs';
    import { FirebaseService } from '../services/firebase.service';
    import { getAuth } from 'firebase/auth';
    
    @Injectable()
    export class HttpRequestInterceptor implements HttpInterceptor {
    
      //AW-Note: Adding this in here to try the appearance of "Persistent Login"
      static accessToken = '';
      static apiKey = 'API_KEY_HERE';
    
      constructor(private firebaseService: FirebaseService, private http: HttpClient) { }
      intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const token = localStorage.getItem('token');
    
        function refreshToken(): Promise<string> {
          // api call returns Promise<string>
          const auth = getAuth();
          const myUser = auth.currentUser;
          return myUser!.getIdToken(true);
        }
    
        async function getNewToken(): Promise<string> {
          const value = await refreshToken() // how to unwrap the value inside this  promise
          return value;
        }
    
    
    
        console.log('Flowing through HTTP Interceptor');
    
        req = req.clone({
          //If you comment out the below line you will no longer send Credentials with the request  
          withCredentials: true,
          setHeaders: { Authorization: `Bearer ${token}` }
        });
    
        return next.handle(req).pipe(/*retry(10),*/ catchError((err: HttpErrorResponse) => {
          console.log("ERROR FOUND! CHECKING TO SEE IF IT IS A 401 ERROR NEXT. Error: " + err);
          if (err.status === 401 || err.status === 0) {
            console.log("NEW TOKEN NEEDED - Yo we got that 401 error going on - let's try and refresh that thang");
    
            //Refresh Token from Firebase
    
    
            console.log("Old Token: " + JSON.stringify(token));
    
    
    
            console.log("Attempting to refresh token now...");
    
            getNewToken()
              .then((newToken) => {
                console.log('New Token: ' + JSON.stringify(newToken));
                localStorage.setItem('token', newToken);
                console.log('Updated Local Storage with new token');
              })
              .then(() => {
                console.log("Here is where the request will refresh");
                return next.handle(req.clone({
                  withCredentials: true,
                  setHeaders: {
                    Authorization: `Bearer ${localStorage.getItem('token')}`
                  }
                }))
              })
              .catch((error) => {
                console.log("Error in Getting new token... Error: " + JSON.stringify(error));
              });
          }
          return throwError(() => err);
        }))
      }
    }
    
    
    export const httpInterceptorProviders = [
      { provide: HTTP_INTERCEPTORS, useClass: HttpRequestInterceptor, multi: true },
    ];