Angular: Auto-Refresh JWT (JSON Web Token) Authentication

The applications that I've implemented in Angular 4+ with HttpClient from @angular/common/http utilize JSON Web Tokens (JWT) for authentication from our server, which are then stored in the Angular app and passed in an Authentication header to the web server for any API calls.

This works well until the JWT expires, in my case after two hours, at which point any HTTP requests start failing because of the expired token.

Rather than having to implement JWT refresh code in every HTTP call, I've instead written a drop-in replacement service for HttpClient that performs the refresh authentication automatically. You call the .get() and .post() methods as you would normally for HttpClient.

The full code can be found here, with a breakdown of what's going on below:

import {Injectable} from '@angular/core';import {HttpClient, HttpErrorResponse} from '@angular/common/http';import {Observable, Subscriber} from 'rxjs';@Injectable()export class CkHttpClientService {  // %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%  // Populate your own Refresh token value here  // %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%  refreshTokenValue: string = null;  // constructor  constructor(private httpClient: HttpClient) { }  // is the specified error an auth error?  private _isAuthError(error: HttpErrorResponse): boolean {    // default that it's not?    let authError = false;    // if an error was included    if (error) {      // check the statuses for an auth error      authError = authError || (error.status === 401);      authError = authError || (error.status === 403);    }    return authError;  }  // refreshes the login token  private _refreshToken(): Observable<boolean> {    const observable = new Observable((subscriber: Subscriber<boolean>) => {      // if there is no refresh token value      if ((this.refreshTokenValue || '') !== '') {        subscriber.next(false);        subscriber.complete();      // if there is a refresh token value      } else {        // %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%        // Your Login Token refresh code goes here        // - Make sure to store the new refresh token for later use        // %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%        this.refreshTokenValue = '';        subscriber.next(true);        subscriber.complete();      }    });    return observable;  }  // performs a GET operation  get(url: string, options?: any): Observable<any> {    return this._http('get', url, null, options);  }  // performs a POST operation  post(url: string, payload: any, options?: any): Observable<any> {    return this._http('post', url, payload, options);  }  // returns the appropriate HttpClient Observable  private _httpClient(method: string, url: string, payload: any, options: any): Observable<any> {    // where we're storing our HTTP Client observable    let httpClientObservable: Observable<any> = null;    // if this is a POST    if (method.toLowerCase() === 'post') {      httpClientObservable = this.httpClient.post(url, payload, options);      // default to a GET    } else {      httpClientObservable = this.httpClient.get(url, options);    }    return httpClientObservable;  }  // generic HTTP caller  private _http(method: string, url: string, payload: any, options: any): Observable<any> {    const observable = Observable.create((subscriber: Subscriber<any>) => {      // execute the HTTP observable      this._httpClient(method, url, payload, options).subscribe((result: any) => {        // on success        subscriber.next(result);        subscriber.complete();      // on error      }, (error: HttpErrorResponse) => {        // is this an auth error?        if (this._isAuthError(error)) {          // call the refresh token function          this._refreshToken().subscribe((refreshSuccessFlag: boolean) => {            // if the refresh was successful            if (refreshSuccessFlag) {              // re-attempt the observable              this._httpClient(method, url, payload, options).subscribe((retryResult: any) => {                // on success, send the result                subscriber.next(retryResult);                subscriber.complete();              // on error              }, (retryError: HttpErrorResponse) => {                subscriber.error(retryError);                subscriber.complete();              });            // if the refresh failed            } else {              subscriber.error(error);              subscriber.complete();            }          });        // if this is not an auth error        } else {          subscriber.error(error);          subscriber.complete();        }      });    });    return observable;  }}

Here is an explanation of what's going on.

refreshTokenValue: string = null;

This is a string variable that stores the login refresh token, and will be used to refresh the token when it expires. You should populate this when the user initially logs in, or you can replace it with a function or getter that refers to another service.

constructor(private httpClient: HttpClient) { }

Here we're injecting httpClient from @angular/common/http.

private _isAuthError(error: HttpErrorResponse): boolean {  // default that it's not?  let authError = false;  // if an error was included  if (error) {    // check the statuses for an auth error    authError = authError || (error.status === 401);    authError = authError || (error.status === 403);  }  return authError;}

This function determines if the HTTP error is an authentication error. We're using the HTTP responses statuses of 401 (Unauthorized) and 403 (Forbidden). We first check that error is a defined object.

private _refreshToken(): Observable<boolean> {  const observable = Observable.create((subscriber: Subscriber<boolean>) => {    // if there is no refresh token value    if ((this.refreshTokenValue || '') !== '') {      subscriber.next(false);      subscriber.complete();    // if there is a refresh token value    } else {      // %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%      // Your Login Token refresh code goes here      // - Make sure to store the new refresh token for later use      // %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%      this.refreshTokenValue = '';      subscriber.next(true);      subscriber.complete();    }  });  return observable;}

If there is no refresh token stored, the function returns false by default. You'll need to swap out the dummy refresh token code here with your own call to refresh the login token.

get(url: string, options?: any): Observable<any> {  return this._http('get', url, null, options);}post(url: string, payload: any, options?: any): Observable<any> {  return this._http('post', url, payload, options);}

These functions mimic HttpClient from @angular/common/http, but internally refer to a common, private _http function, since the code is mostly identical. I haven't implemented OPTIONS or PUT or any other methods, but you should be able to yourself.

private _httpClient(method: string, url: string, payload: any, options: any): Observable<any> {  // where we're storing our HTTP Client observable  let httpClientObservable: Observable<any> = null;  // if this is a POST  if (method.toLowerCase() === 'post') {    httpClientObservable = this.httpClient.post(url, payload, options);  // default to a GET  } else {    httpClientObservable = this.httpClient.get(url, options);  }  return httpClientObservable;}

This function returns the appropriate GET or POST Observable, based on the method value passed to it. We'll be using this function multiple times in _http().

private _http(method: string, url: string, payload: any, options: any): Observable<any> {  const observable = Observable.create((subscriber: Subscriber<any>) => {

The bulk of the heavy lifting is done in this function, performing the initial HTTP request, refreshing the token if we receive an authentication error, and performing the subsequent HTTP request after the token is refreshed.

    this._httpClient(method, url, payload, options).subscribe((result: any) => {      // on success      subscriber.next(result);      subscriber.complete();    // on error    }, (error: HttpErrorResponse) => {      // is this an auth error?      if (this._isAuthError(error)) {

We first try performing the initial HTTP request. If it was successful, we pass through the result. Otherwise if it errors, we test to see if it was an authentication error.

        // call the refresh token function        this._refreshToken().subscribe((refreshSuccessFlag: boolean) => {

If it was an authentication error, we call the _refreshToken() function to perform the refresh and return a success indicator.

          // if the refresh was successful          if (refreshSuccessFlag) {            // re-attempt the observable            this._httpClient(method, url, payload, options).subscribe((retryResult: any) => {              // on success, send the result              subscriber.next(retryResult);              subscriber.complete();

If the refresh was successful, we re-attempt the HTTP request that we performed earlier, passing through the result if it was successful.

            // on error            }, (retryError: HttpErrorResponse) => {              subscriber.error(retryError);              subscriber.complete();            });

If the HTTP request was unsuccessful, we pass through the error.

          // if the refresh failed          } else {            subscriber.error(error);            subscriber.complete();          }        });

If the token refresh was not successful, pass through that error.

      // if this is not an auth error      } else {        subscriber.error(error);        subscriber.complete();      }

If the initial HTTP error wasn't an authentication error, we pass that error through.