1

I'm working on an Angular application with an HTTP interceptor for handling authentication tokens. I'm facing a couple of challenges and need guidance on best practices.

The interceptor currently checks if the request is a refresh token request using a separate function (isRefreshTokenRequest). I'd like to simplify the interceptor by removing this check. However, I realize that removing this check could potentially cause an infinite loop, as the refresh token request itself would trigger the interceptor. How can I handle this situation without explicitly checking for the refresh token endpoint?

import { inject } from '@angular/core';
import { HttpErrorResponse, HttpEvent, HttpHandlerFn, HttpRequest } from '@angular/common/http';
import { BehaviorSubject, catchError, debounceTime, filter, Observable, switchMap, take, throwError } from 'rxjs';
import { AuthService } from '../services/auth/auth.service';
import { AuthResponse } from '../interfaces/auth-response';

let isRefreshing = false;
const refreshTokenSubject: BehaviorSubject<string | null> = new BehaviorSubject<string | null>(null);

export function authInterceptor(request: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
  const authService = inject(AuthService);

  if (authService.accessToken()) {
    request = addToken(request, authService.accessToken()!);
  }

  if (isRefreshTokenRequest(request)) {
    return next(request);
  }

  function handle401Error(request: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
    if (!isRefreshing) {
      isRefreshing = true;
      refreshTokenSubject.next(null);

      return authService.refreshToken().pipe(
        switchMap((response: AuthResponse) => {
          isRefreshing = false;
          refreshTokenSubject.next(response.accessToken);
          return next(addToken(request, response.accessToken));
        }),
        catchError((error) => {
          isRefreshing = false;
          if (
            error instanceof HttpErrorResponse &&
            error.status === 401 &&
            (error.error?.message === 'Token expired' || error.error?.message === 'Invalid token')
          ) {
            authService.logOut();
          }
          return throwError(() => error);
        }),
      );
    }
    return refreshTokenSubject.pipe(
      filter((token) => token !== null),
      take(1),
      switchMap((token) => next(addToken(request, token!))),
    );
  }

  return next(request).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        return handle401Error(request, next);
      }
      return throwError(() => error);
    }),
  );
}

function addToken(request: HttpRequest<unknown>, token: string) {
  return request.clone({
    setHeaders: {
      Authorization: `Bearer ${token}`,
    },
  });
}

function isRefreshTokenRequest(request: HttpRequest<unknown>): boolean {
  return request.url.includes('/api/authentication/refresh-token');
}
1
  • this link was helpful Commented Jul 20, 2024 at 1:22

1 Answer 1

0

You need to use HttpBackend, where calls made through this are not intercepted by the interceptor, hence preventing the infinite loop.

From the docs:

A final HttpHandler which will dispatch the request via browser HTTP APIs to a backend.

Interceptors sit between the HttpClient interface and the HttpBackend.

When injected, HttpBackend dispatches requests directly to the backend, without going through the interceptor chain.

...
constructor(private httpBackend: HttpBackend) {}

refreshToken() {
     return this.httpBackend.get('route');
}
...
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.