Using HTTP Interceptors for JWT in Angular Applications

In modern Angular applications, authentication tokens such as JWTs (JSON Web Tokens) are essential for securing API calls. HTTP interceptors provide a centralized way to attach these tokens to all outgoing requests automatically, ensuring consistent authentication and reducing repetitive code in services and components.

This post provides a comprehensive guide to setting up JWT HTTP interceptors, covering everything from basic usage to advanced features like error handling, token refresh, and modular implementation.

1. Understanding HTTP Interceptors in Angular

Angular HTTP interceptors are a part of the HttpClient module. They act as middleware for outgoing HTTP requests and incoming responses. Interceptors can:

  • Modify request headers (e.g., attach JWT tokens).
  • Handle errors globally.
  • Log requests and responses for debugging.
  • Transform request or response data.

By using interceptors, you avoid adding authentication headers manually in each service method.


2. Basic JWT Interceptor Setup

To create an interceptor, implement the HttpInterceptor interface:

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
const token = localStorage.getItem('token');
if (token) {
  req = req.clone({
    setHeaders: { Authorization: Bearer ${token} }
  });
}
return next.handle(req);
} }
  • req.clone() creates a new request with the Authorization header.
  • next.handle(req) forwards the request to the server.

3. Registering the Interceptor

Add the interceptor in app.module.ts using the HTTP_INTERCEPTORS token:

import { HTTP_INTERCEPTORS } from '@angular/common/http';

@NgModule({
  providers: [
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }
] }) export class AppModule {}
  • multi: true allows multiple interceptors to coexist.
  • The interceptor now runs automatically for all HTTP requests.

4. Using the AuthService with Interceptors

When combined with a login service, the interceptor ensures all requests carry the JWT token:

@Injectable({ providedIn: 'root' })
export class AuthService {
  login(username: string, password: string) {
return this.http.post&lt;{ token: string }&gt;('/api/login', { username, password })
  .pipe(tap(res =&gt; localStorage.setItem('token', res.token)));
} logout() {
localStorage.removeItem('token');
} }

No additional headers are needed in service methods:

getUserData() {
  return this.http.get('/api/user');
}

The interceptor automatically attaches the JWT from localStorage.


5. Handling Token Expiration

Tokens often have a limited lifetime. To handle expired tokens:

  1. Detect 401 Unauthorized responses.
  2. Refresh the token if possible.
  3. Retry the failed request.

Example:

import { catchError, switchMap } from 'rxjs/operators';
import { throwError, of } from 'rxjs';

intercept(req: HttpRequest<any>, next: HttpHandler) {
  const token = localStorage.getItem('token');
  if (token) {
req = req.clone({ setHeaders: { Authorization: Bearer ${token} } });
} return next.handle(req).pipe(
catchError(err =&gt; {
  if (err.status === 401) {
    return this.authService.refreshToken().pipe(
      switchMap(newToken =&gt; {
        req = req.clone({ setHeaders: { Authorization: Bearer ${newToken} } });
        return next.handle(req);
      })
    );
  }
  return throwError(() =&gt; err);
})
); }
  • Uses catchError to detect 401 responses.
  • switchMap retries the request with the new token.

6. Logging and Debugging Requests

Interceptors can also log requests for debugging purposes:

intercept(req: HttpRequest<any>, next: HttpHandler) {
  console.log('Outgoing request:', req.url, req.headers);
  return next.handle(req).pipe(
tap(event =&gt; console.log('Response received:', event))
); }

This helps identify issues like missing headers or server errors.


7. Multiple Interceptors

You can chain multiple interceptors for different tasks:

providers: [
  { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }
]
  • JWT interceptor attaches tokens.
  • Logging interceptor logs requests/responses.
  • Execution order is the order of registration.

8. Conditionally Attaching Tokens

Sometimes, you may not want to attach a token for public endpoints:

intercept(req: HttpRequest<any>, next: HttpHandler) {
  if (req.url.includes('/public')) {
return next.handle(req);
} const token = localStorage.getItem('token'); if (token) {
req = req.clone({ setHeaders: { Authorization: Bearer ${token} } });
} return next.handle(req); }

This avoids sending unnecessary tokens to endpoints that don’t require authentication.


9. Using Interceptors with Angular Services

Example service using HTTP methods without manually adding headers:

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private http: HttpClient) {}

  getProfile() {
return this.http.get('/api/profile');
} updateProfile(data: any) {
return this.http.put('/api/profile', data);
} }

All requests automatically carry the JWT via the interceptor.


10. Testing JWT Interceptors

Use Angular’s testing utilities to verify headers:

it('should attach JWT token to requests', () => {
  const interceptor: JwtInterceptor = TestBed.inject(JwtInterceptor);
  localStorage.setItem('token', '12345');

  const httpRequest = new HttpRequest('GET', '/api/test');
  interceptor.intercept(httpRequest, {
handle: req =&gt; {
  expect(req.headers.get('Authorization')).toBe('Bearer 12345');
  return of({});
}
}).subscribe(); });
  • Ensures the interceptor attaches tokens correctly.

11. Best Practices for JWT Interceptors

  1. Centralize token management in a service.
  2. Use interceptors to attach tokens automatically.
  3. Handle token expiration with refresh logic.
  4. Avoid sending tokens to public endpoints.
  5. Chain interceptors for logging, error handling, and token attachment.
  6. Secure token storage (localStorage, sessionStorage, or cookies with HTTP-only flags).
  7. Test interceptors to ensure reliability.

12. Advanced Features

  • Token Refresh Queue: Ensure multiple requests waiting for a refreshed token don’t trigger multiple refresh calls.
  • Global Error Handling: Interceptor can catch errors like 401 and 403 to redirect users to login.
  • Custom Headers: Combine JWT with other headers like Content-Type or Accept.

13. Example: Complete JWT Interceptor with Refresh

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
  private isRefreshing = false;

  constructor(private authService: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
const token = this.authService.getToken();
if (token) req = req.clone({ setHeaders: { Authorization: Bearer ${token} } });
return next.handle(req).pipe(
  catchError(err =&gt; {
    if (err.status === 401 &amp;&amp; !this.isRefreshing) {
      this.isRefreshing = true;
      return this.authService.refreshToken().pipe(
        switchMap(newToken =&gt; {
          this.isRefreshing = false;
          req = req.clone({ setHeaders: { Authorization: Bearer ${newToken} } });
          return next.handle(req);
        })
      );
    }
    return throwError(() =&gt; err);
  })
);
} }
  • Ensures only one refresh request is sent at a time.
  • All failed requests are retried with the new token.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *