Refresh Token not being refreshed on 401 error code

Every single tutorial and snippet is using this kind of refresh token code: https://gist.github.com/abereghici/054cefbdcd8ccd3ff03dcc4e5155242b which simply won’t work in my case because this.authService.refreshToken() does HTTP post request which won’t execute the call until a .subscribe() is made. When I add the subscribe call, it calls the http post in a new request but it’s all messy into my head. Can you please explain and tell me how to correctly deal with that? Please don’t link me snippets that I’ve already seen.

Information about my ASP.NET Core 2.2 Web API back-end:

  • /api/auth/login returns AccessToken (JWT Token + Refresh Token strings). The JWT token expires in 1 minutes only for test purposes. The Refresh Token expires in 10 minutes.
  • /api/auth/token/refresh returns a new AccessToken. Both JWT and Refresh tokens are recreated. Another important thing is that if the Refresh Token expires and a refreshing request is made after that, it returns a BadRequest which should be handled in the angular part, probably somehow with a catchError() inside the pipe.
  • /api/auth/logout will revoke the Refresh Token. So basically, there will be http post request on this.authService.logout() too even tho it’s not in the code I’ve given below.

auth.interceptor.ts

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

import { AuthService } from '../services/auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  private refreshTokenInProgress = false;
  private tokenSubject = new BehaviorSubject<any>(null);

  constructor(private authService: AuthService) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request)
      .pipe(
        catchError((error: HttpErrorResponse) => {

          if (error instanceof HttpErrorResponse) {
            if (error.status === 401) {
              return this.handleHttpResponseError(request, next);
            }
          }

          return throwError(error);
        })
      );
  }

  private handleHttpResponseError(request: HttpRequest<any>, next: HttpHandler) {
    const accessToken = this.authService.getJwtToken();

    // if jwt token is not set, we just let the request execute
    if (!accessToken) {
      return next.handle(request);
    }

    // if jwt token has not expired yet, we add the authorize header
    // otherwise we refresh the token
    if (!this.authService.isTokenExpired()) {
      console.log('added authorization to the headers')

      return next.handle(this.attachTokenToRequest(request, accessToken));
    } else {
      console.log('token expired');

      this.authService.refreshToken()
        .pipe(
          switchMap(result => {
            console.log(`API returned: ${result}`);

            return next.handle(this.attachTokenToRequest(request, accessToken));
          })
        );

      return next.handle(request);
    }
  }

  private attachTokenToRequest(request: HttpRequest<any>, token: string) {
    return request.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
  }
}

auth.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';
import { map } from 'rxjs/operators';
import { JwtHelperService } from '@auth0/angular-jwt';

import { AccessToken } from '../types/user';

@Injectable()
export class AuthService {
  private actionUrl: string;
  private readonly JWT_TOKEN = 'access_token';
  private readonly REFRESH_TOKEN = 'refresh_token';

  constructor(private httpClient: HttpClient) {
    this.actionUrl = `${environment.baseUrls.server}api/auth`;
  }

  isLoggedIn(): boolean {
    return !!this.getJwtToken();
  }

  login(credentials: { email: string, password: string }) {
    return this.httpClient.post<AccessToken>(`${this.actionUrl}/login`, credentials)
      .pipe(
        map(tokens => {
          this.setJwtToken(tokens.accessToken);
          this.setRefreshToken(tokens.refreshToken);
          return true;
        })
      );
  }

  refreshToken() {
    console.log('refreshToken() called');

    const helper = new JwtHelperService();
    const email = helper.decodeToken(this.getJwtToken()).sub;

    return this.httpClient.post<AccessToken>(`${this.actionUrl}/token/refresh`, { 'refreshToken': localStorage.getItem(this.REFRESH_TOKEN), 'email': email })
      .pipe(
        map(tokens => {
          console.log('refreshToken().pipe called()');

          this.setJwtToken(tokens.accessToken);
          this.setRefreshToken(tokens.refreshToken);
          return true;
        })
      );
  }

  logout() {
    localStorage.removeItem(this.JWT_TOKEN);
    localStorage.removeItem(this.REFRESH_TOKEN);
  }

  isTokenExpired(): boolean {
    const helper = new JwtHelperService();
    return helper.isTokenExpired(this.getJwtToken());
  }

  getJwtToken(): string {
    return localStorage.getItem(this.JWT_TOKEN);
  }

  setJwtToken(token: string) {
    localStorage.setItem(this.JWT_TOKEN, token);
  }

  getRefreshToken(): string {
    return localStorage.getItem(this.REFRESH_TOKEN);
  }

  setRefreshToken(token: string) {
    localStorage.setItem(this.REFRESH_TOKEN, token);
  }
}

Source: New feed
Source Url Refresh Token not being refreshed on 401 error code