Interceptores para autenticación

Avanzado
Angular
Angular
Actualizado: 24/09/2025

Interceptor de autenticación con JWT

Los interceptores de autenticación representan uno de los patrones más fundamentales en aplicaciones empresariales modernas. Su función principal consiste en agregar automáticamente tokens de autorización a las peticiones HTTP salientes, eliminando la necesidad de hacerlo manualmente en cada servicio.

Creación del interceptor de autenticación

Un interceptor funcional para autenticación JWT sigue un patrón específico que aprovecha la inyección de dependencias moderna de Angular. El interceptor debe acceder al token almacenado y agregarlo como header Authorization:

import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).getToken();
  
  // Si no hay token, continuar sin modificar la request
  if (!token) {
    return next(req);
  }
  
  // Clonar la request y agregar el header Authorization
  const authReq = req.clone({
    setHeaders: {
      Authorization: `Bearer ${token}`
    }
  });
  
  return next(authReq);
};

La función inject() nos permite acceder al AuthService directamente dentro del interceptor funcional, siguiendo el patrón de inyección moderna de Angular 20+.

Servicio de autenticación básico

Para que el interceptor funcione correctamente, necesitamos un AuthService que gestione el almacenamiento y recuperación del token JWT:

import { Injectable, signal } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private readonly tokenKey = 'auth_token';
  private readonly tokenSignal = signal<string | null>(null);
  
  constructor() {
    // Cargar token desde localStorage al inicializar
    const storedToken = localStorage.getItem(this.tokenKey);
    if (storedToken) {
      this.tokenSignal.set(storedToken);
    }
  }
  
  getToken(): string | null {
    return this.tokenSignal();
  }
  
  setToken(token: string): void {
    localStorage.setItem(this.tokenKey, token);
    this.tokenSignal.set(token);
  }
  
  clearToken(): void {
    localStorage.removeItem(this.tokenKey);
    this.tokenSignal.set(null);
  }
  
  isAuthenticated(): boolean {
    return !!this.getToken();
  }
}

Este servicio utiliza signals para mantener el estado reactivo del token y localStorage para persistencia entre sesiones del navegador.

Configuración del interceptor

El interceptor debe registrarse en la configuración de la aplicación usando withInterceptors:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { authInterceptor } from './app/interceptors/auth.interceptor';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor])
    )
  ]
});

Exclusión de endpoints públicos

No todas las peticiones requieren autenticación. Los endpoints públicos como login, registro o recursos estáticos deben excluirse del interceptor:

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).getToken();
  
  // Lista de endpoints que no requieren autenticación
  const publicEndpoints = ['/auth/login', '/auth/register', '/public'];
  const isPublicEndpoint = publicEndpoints.some(endpoint => 
    req.url.includes(endpoint)
  );
  
  // Si es endpoint público o no hay token, continuar sin modificar
  if (isPublicEndpoint || !token) {
    return next(req);
  }
  
  const authReq = req.clone({
    setHeaders: {
      Authorization: `Bearer ${token}`
    }
  });
  
  return next(authReq);
};

Integración con el flujo de login

El AuthService se integra naturalmente con el proceso de autenticación. Cuando el usuario se autentica exitosamente, el token se almacena y estará disponible automáticamente para el interceptor:

@Injectable({
  providedIn: 'root'
})
export class LoginService {
  private http = inject(HttpClient);
  private authService = inject(AuthService);
  
  login(credentials: LoginCredentials) {
    return this.http.post<AuthResponse>('/auth/login', credentials)
      .pipe(
        tap(response => {
          // El interceptor NO afectará esta petición por ser /auth/login
          this.authService.setToken(response.accessToken);
        })
      );
  }
}

Una vez almacenado el token, todas las peticiones posteriores incluirán automáticamente el header Authorization sin requerir modificaciones en los servicios existentes.

Validación de formato del token

Para mayor robustez, el interceptor puede incluir validación básica del formato JWT antes de agregarlo a las peticiones:

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).getToken();
  
  if (!token || !isValidJWTFormat(token)) {
    return next(req);
  }
  
  const authReq = req.clone({
    setHeaders: {
      Authorization: `Bearer ${token}`
    }
  });
  
  return next(authReq);
};

function isValidJWTFormat(token: string): boolean {
  // JWT tiene 3 partes separadas por puntos
  const parts = token.split('.');
  return parts.length === 3 && parts.every(part => part.length > 0);
}

Esta aproximación funcional moderna elimina la complejidad de los interceptores basados en clases, proporcionando una solución limpia y mantenible para la autenticación automática en aplicaciones Angular 20+.

Manejo de refresh tokens y errores 401

El manejo de refresh tokens representa el siguiente nivel de sofisticación en interceptores de autenticación. Cuando un token JWT expira, el servidor responde con error 401, y necesitamos renovar automáticamente el token sin interrumpir la experiencia del usuario.

Interceptor de respuesta para errores 401

Los interceptores funcionales pueden manejar tanto requests como responses. Para gestionar errores 401, necesitamos interceptar las respuestas y detectar cuándo el token ha expirado:

import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, switchMap, throwError } from 'rxjs';

export const refreshTokenInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  
  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      // Solo manejar errores 401 Unauthorized
      if (error.status === 401 && authService.getRefreshToken()) {
        return handleTokenRefresh(req, next, authService);
      }
      
      // Para otros errores, propagarlos normalmente
      return throwError(() => error);
    })
  );
};

Servicio de autenticación extendido

El AuthService debe ampliarse para manejar refresh tokens y proporcionar métodos de renovación:

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private readonly tokenKey = 'auth_token';
  private readonly refreshTokenKey = 'refresh_token';
  private readonly tokenSignal = signal<string | null>(null);
  private readonly refreshTokenSignal = signal<string | null>(null);
  
  private isRefreshing = false;
  private refreshTokenSubject = new BehaviorSubject<string | null>(null);
  
  getRefreshToken(): string | null {
    return this.refreshTokenSignal() || localStorage.getItem(this.refreshTokenKey);
  }
  
  setTokens(accessToken: string, refreshToken: string): void {
    localStorage.setItem(this.tokenKey, accessToken);
    localStorage.setItem(this.refreshTokenKey, refreshToken);
    this.tokenSignal.set(accessToken);
    this.refreshTokenSignal.set(refreshToken);
  }
  
  refreshAccessToken(): Observable<AuthResponse> {
    const refreshToken = this.getRefreshToken();
    
    if (!refreshToken) {
      return throwError(() => new Error('No refresh token available'));
    }
    
    return this.http.post<AuthResponse>('/auth/refresh', { refreshToken })
      .pipe(
        tap(response => {
          this.setTokens(response.accessToken, response.refreshToken);
          this.refreshTokenSubject.next(response.accessToken);
        }),
        catchError(error => {
          // Si el refresh token también expiró, limpiar todo
          this.logout();
          return throwError(() => error);
        })
      );
  }
}

Lógica de renovación automática

La función handleTokenRefresh gestiona el proceso complejo de renovar tokens y reintentar la petición original:

function handleTokenRefresh(
  req: HttpRequest<any>, 
  next: HttpHandlerFn, 
  authService: AuthService
): Observable<HttpEvent<any>> {
  
  // Si ya estamos renovando, esperar al resultado
  if (authService.isRefreshing) {
    return authService.refreshTokenSubject.pipe(
      filter(token => token !== null),
      take(1),
      switchMap(token => {
        // Reintentar la petición original con el nuevo token
        const newReq = addTokenToRequest(req, token);
        return next(newReq);
      })
    );
  }
  
  // Iniciar proceso de renovación
  authService.isRefreshing = true;
  authService.refreshTokenSubject.next(null);
  
  return authService.refreshAccessToken().pipe(
    switchMap(response => {
      authService.isRefreshing = false;
      authService.refreshTokenSubject.next(response.accessToken);
      
      // Reintentar la petición original
      const newReq = addTokenToRequest(req, response.accessToken);
      return next(newReq);
    }),
    catchError(error => {
      authService.isRefreshing = false;
      authService.refreshTokenSubject.next(null);
      
      // Redireccionar al login si la renovación falla
      inject(Router).navigate(['/login']);
      return throwError(() => error);
    })
  );
}

Función auxiliar para agregar tokens

Una función helper simplifica la adición del token a las peticiones HTTP:

function addTokenToRequest(req: HttpRequest<any>, token: string): HttpRequest<any> {
  return req.clone({
    setHeaders: {
      Authorization: `Bearer ${token}`
    }
  });
}

Configuración de múltiples interceptores

Los interceptores deben ordenarse correctamente para que funcionen en secuencia: primero agregar el token, luego manejar errores de renovación:

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([
        authInterceptor,           // Agrega token a requests
        refreshTokenInterceptor    // Maneja renovación en responses
      ])
    )
  ]
});

Manejo de múltiples peticiones simultáneas

Cuando múltiples peticiones fallan simultáneamente por token expirado, solo debe ejecutarse una renovación. El patrón con BehaviorSubject evita renovaciones duplicadas:

// En el AuthService
private isRefreshing = false;
private refreshTokenSubject = new BehaviorSubject<string | null>(null);

// Las peticiones en espera se suscriben al subject
// Solo la primera ejecuta refreshAccessToken()
// Las demás esperan el resultado compartido

Exclusión de endpoints de renovación

El interceptor de refresh debe excluir los endpoints de renovación para evitar bucles infinitos:

export const refreshTokenInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  
  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      // No manejar errores en endpoints de auth
      const isAuthEndpoint = req.url.includes('/auth/');
      
      if (error.status === 401 && !isAuthEndpoint && authService.getRefreshToken()) {
        return handleTokenRefresh(req, next, authService);
      }
      
      return throwError(() => error);
    })
  );
};

Limpieza al cerrar sesión

Cuando el usuario cierra sesión o los tokens no pueden renovarse, el sistema debe limpiar completamente el estado:

logout(): void {
  localStorage.removeItem(this.tokenKey);
  localStorage.removeItem(this.refreshTokenKey);
  this.tokenSignal.set(null);
  this.refreshTokenSignal.set(null);
  this.isRefreshing = false;
  this.refreshTokenSubject.next(null);
  
  inject(Router).navigate(['/login']);
}

Esta implementación proporciona una experiencia de usuario fluida donde los tokens se renuevan automáticamente sin interrumpir el flujo de trabajo, manteniendo la seguridad mediante la rotación regular de credenciales de acceso.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Angular

Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender el uso de interceptores funcionales para añadir tokens JWT a peticiones HTTP.
  • Implementar un servicio de autenticación que gestione tokens y su persistencia.
  • Configurar interceptores para excluir endpoints públicos y manejar errores 401.
  • Desarrollar lógica para la renovación automática de tokens mediante refresh tokens.
  • Gestionar múltiples peticiones simultáneas y limpiar el estado al cerrar sesión.