Handling JWT Authentication & Refresh Tokens in Angular Using an HTTP Interceptor

작성자

카테고리:

← 피드로
DEV Community · harshvardhan · 2026-08-11 개발(SW)

While working on authentication in Angular, one thing I found particularly useful was using an HTTP interceptor to handle authentication automatically.

Instead of manually adding the access token to every API request, the interceptor can:

Attach the JWT access token to outgoing requests
Detect 401 Unauthorized responses
Use the refresh token to obtain a new access token
Retry the failed request automatically
Logout the user if the refresh token is expired/invalid

Here’s the approach I used.

  1. Adding the Access Token to Requests

First, I retrieve the authentication data from localStorage:

const getAuth = () => {
  const stored = localStorage.getItem('auth');
  return stored ? JSON.parse(stored) : null;
};

const auth = getAuth();
let authReq = req;

Enter fullscreen mode Exit fullscreen mode

If an access token exists, I clone the request and add the Authorization header:

if (auth?.accessToken) {
  authReq = req.clone({
    setHeaders: {
      Authorization: `Bearer ${auth.accessToken}`,
    },
  });
}

Enter fullscreen mode Exit fullscreen mode

Now every API request automatically contains:

Authorization: Bearer

This means I don’t need to manually add the token in every service method.

  1. Handling 401 Unauthorized

The access token is usually short-lived for security reasons.

Eventually, it can expire and the API may respond with:

401 Unauthorized

Instead of immediately logging the user out, I check whether a refresh token is available:

if (error.status === 401 && auth?.refreshToken)

Then I call my refresh-token endpoint:

return authService.refreshToken().pipe

The backend validates the refresh token and returns a new access token.

  1. Retrying the Failed Request

Once I receive the new tokens, I store them:

switchMap((newTokens: any) => {
authService.setUser(newTokens);

Then I create a new version of the original request using the new access token:

const retryReq = req.clone({
setHeaders: {
Authorization: Bearer ${newTokens.accessToken},
},
});

return next(retryReq);

So the flow becomes:

API Request

Access Token

Server

401 Unauthorized

Refresh Token

New Access Token

Retry Original Request

The user doesn’t have to manually log in again.

  1. What Happens If Refresh Fails?

The refresh token itself can also expire or become invalid.

In that situation, the refresh request will fail.

I handle that using another catchError:

catchError((refreshError) => {
  authService.clearUser();
  router.navigate(['/login']);

  return throwError(() => refreshError);
})

Enter fullscreen mode Exit fullscreen mode

The authentication data is removed and the user is redirected to the login page.

  1. Important: Don’t Refresh the Refresh Request

There is one edge case we need to handle.

Suppose /Auth/Refresh itself returns 401.

If our interceptor tries to refresh that request again, we could end up with an infinite loop.

So I check:

const isRefreshRequest = req.url.includes('/Auth/Refresh');

Enter fullscreen mode Exit fullscreen mode

If the failed request is already the refresh request, I immediately logout:

if (isRefreshRequest) {
  authService.clearUser();
  router.navigate(['/login']);

  return throwError(() => error);
}

Enter fullscreen mode Exit fullscreen mode

This prevents the interceptor from repeatedly trying to refresh an already-invalid refresh token.

Putting everything together:

import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../service/auth-service';
import { Router } from '@angular/router';
import { catchError, switchMap, throwError } from 'rxjs';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  const getAuth = () => {
    const stored = localStorage.getItem('auth');
    return stored ? JSON.parse(stored) : null;
  };

  const auth = getAuth();
  let authReq = req;

  if (auth?.accessToken) {
    authReq = req.clone({
      setHeaders: {
        Authorization: `Bearer ${auth.accessToken}`,
      },
    });
  }

  return next(authReq).pipe(
    catchError((error: HttpErrorResponse) => {
      const isRefreshRequest = req.url.includes('/Auth/Refresh');

      if (isRefreshRequest) {
        authService.clearUser();
        router.navigate(['/login']);
        return throwError(() => error);
      }

      if (error.status === 401 && auth?.refreshToken) {
        return authService.refreshToken().pipe(
          switchMap((newTokens: any) => {
            authService.setUser(newTokens);

            const retryReq = req.clone({
              setHeaders: {
                Authorization: `Bearer ${newTokens.accessToken}`,
              },
            });
            return next(retryReq);
          }),
          catchError((refreshError) => {
            authService.clearUser();
            router.navigate(['/login']);
            return throwError(() => refreshError);
          }),
        );
      }
      return throwError(() => error);
    }),
  );
};

Enter fullscreen mode Exit fullscreen mode

The main advantage is that authentication logic stays in one place.

Without an interceptor, every API service might need to deal with:

Get token

Add Authorization header

Make request

Check 401

Refresh token

Retry request

With an interceptor, the individual services can stay focused on their actual responsibilities and The interceptor takes care of authentication automatically.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다