1
votes

Within my app I am using the Angular HttpInterceptor to track each outgoing HTTP request and add a JWT token stored in local storage to the request headers so that the backend API can authorize the request by checking the JWT token.

To do this, the interceptor simply takes the HTTP request and adds the token by setting the headers and then calls next.handle(req) to propagate the request.

This is done using the below code:

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    req = this.addHeaders(req);
    return next.handle(req).pipe(
      catchError(error => {
        if (error instanceof HttpErrorResponse && error.status === 401) {
          this._authService.signOut("unauthorized");
        }
        if (error instanceof HttpErrorResponse && error.status === 403) {
          this._authService.signOut("expired");
        }
        return throwError(error);
      })
    );
  }

  /**
   * Add access_token to header.
   * @param req
   */
  addHeaders(req: HttpRequest<any>): HttpRequest<any> {
    return req.clone({
      setHeaders: {
        "Content-type": "application/json",
        token: this._authService.getAuthenticationToken(),
        jwt_token: this._authService.getAuthorizationToken(),
        [this._config.subscriptionKeyName]: this._config.subscriptionKey || ""
      }
    });
  }

I want to extend this functionality so that the interceptor first checks to ensure that the JWT token stored in browser local storage is valid. If the token is not valid, the interceptor itself should fire off a separate HTTP request to retrieve a new JWT token from an API endpoint which should be used to overwrite the old one before continuing with the original request.

I'm trying to figure out how to construct this behaviour in my Angular code, but I keep running into infinite loops.

This is the code I have at the moment.

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

    if (req.headers.has("newtokenrequest")) {
      next.handle(req);
    }

    let jwtToken: string = this._authService.getLocalAuthorizationToken();

    if (!this.checkJwtTokenIsValid(jwtToken)) { //Invalid token, get new one
      const authTokenStream = this._authService.getApiAuthorizationToken$();
      authTokenStream.subscribe(token => {
        this._authService.setAuthorizationToken(token);
      });

    }

    req = this.addHeaders(req);
    return next.handle(req).pipe(
      catchError(error => {
        if (error instanceof HttpErrorResponse && error.status === 401) {
          this._authService.signOut("unauthorized");
        }
        if (error instanceof HttpErrorResponse && error.status === 403) {
          this._authService.signOut("expired");
        }
        return throwError(error);
      })
    );
  }

And this is the code that returns the Observable<string> that retrieves the new JWT token.

getApiAuthorizationToken$(): Observable<string> {
    let headers = new HttpHeaders();
    headers = headers.set('newtokenrequest', 'true');
    return this._httpClient.get<string>(this._configService.getConfig().teacherAuthAPI, {headers: headers});
  }

The code gets down to the authTokenStream subscription and then loops back again to the top. I assume this is because the subscription is sending off a new HTTP request to the backend to retrieve the token, however this request should include the newtokenrequest as a header, which should be detected by the intercept method which should call next.handle(req), allowing the request to continue on with no further interaction, however this doesn't happen and instead the code loops instead.

Edit: Update

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

    if (req.headers.has("newtokenrequest")) {
      return next.handle(req);
    }

    let jwtToken: string = this._authService.getLocalAuthorizationToken();

    if (!this.checkJwtTokenIsValid(jwtToken) || jwtToken === undefined) { //Invalid token, get new one
      const authTokenStream = this._authService.getApiAuthorizationToken$();
      authTokenStream.subscribe(token => {
        this._authService.setAuthorizationToken(token);
        return next.handle(req);
      });
    }

    req = this.addHeaders(req);
    return next.handle(req).pipe(
      catchError(error => {
        if (error instanceof HttpErrorResponse && error.status === 401) {
          this._authService.signOut("unauthorized");
        }
        if (error instanceof HttpErrorResponse && error.status === 403) {
          this._authService.signOut("expired");
        }
        return throwError(error);
      })
    );
  }

  getApiAuthorizationToken$(): Observable<string> {
    let headers = new HttpHeaders({
      newtokenrequest: 'true'
    });
    // return this._httpClient.get<string>(this._configService.getConfig().teacherAuthAPI, {headers: headers});
    const req = new HttpRequest<string>('GET', this._configService.getConfig().teacherAuthAPI, {headers});
    let res = this._httpBackend.handle(req).pipe(map((response: HttpResponse<string>) => response.body),);
    return res;
  }
2
Why would you check the token before sending it ? The client app should be the dumbest possible, all of your logic should be on the back-end. The API has a myriad of ways for validating the token or refreshing it. Simply send the token, and let it deal with it ? - user4676340
are you looking for itnext.io/… - jitender
Your Angular code should not check the validity of the JWT token. Is the backend service the responsible of that task, returning an error (probably 401 Unauthorized). The Angular application should then redirect to a login component or call an authorization service that will provide another token. By the way, have you tried @auth0/angular-jwt? Is easy to add to your application and you wouldn't need the interceptor anymore. - Nemezih

2 Answers

0
votes

What you should do, is to assign the work of checking the localstorage validity to an authguard.

canActivate(): Observable<boolean> | Promise<boolean> | boolean {

        switch (this.ispectUrl(this.url)) {
    // I'm checking if there is a jwt in url
          case true: {
            const token = this.refactorUrl(this.url);
            this.auth.setAccessToken(token);
            return this.auth.validateJwt();
            break;
          }
    // No jwt in url? I check possibile jwt in localstorage
          case false: {
            return this.auth.validateJwt();
            break;
          }
    // Neither of both, simply i logout to login page session expired
          default: {
            this.auth.logout();
            return false;
          }

        }
    }

Here i'm extracting the jwt from the url, because i work with a CAS implementation, but if there is not any jwt on url i simply check the localstorage before doing anithing, if the jwt is valid the user can navigate all the url protected from the authguard, remember that after checking the jwt validity you should extract the jwt expiration date, and from that you should know when to check for refresh.

0
votes

You can avoid looping while requesting the authorization token by using HttpBackend service instead of HttpClient. HttpBackend is a low-level Http service used by HttpClient. It does not consider interceptors.

import {HttpBackend, HttpHeaders, HttpRequest, HttpResponse} from '@angular/common/http';

constructor(private httpBackend: HttpBackend) {
}

getApiAuthorizationToken$(): Observable<string> {
  const headers = new HttpHeaders({
    newtokenrequest: 'true'
  });

  const request = new HttpRequest<string>(
    'GET',
    this._configService.getConfig().teacherAuthAPI,
    {
      headers
    });

  return this.httpBackend.handle(request).pipe(
    map((response: HttpResponse<string>) => response.body),
  );
}

UPD.

Replace

const authTokenStream = this._authService.getApiAuthorizationToken$();
authTokenStream.subscribe(token => {
  this._authService.setAuthorizationToken(token);
  return next.handle(req);
});

with

return this.this._authService.getApiAuthorizationToken$().pipe(
  tap(token => this._authService.setAuthorizationToken(token)),
  switchMap(() => {
    req = this.addHeaders(req);
    return next.handle(req);
  })
);

Try to avoid calling subscribe inside the interceptor.