401 Unauthorized vs 403 Forbidden

Fundamentals Security Notes Jan 9, 2026 HTTP
http status-codes authentication authorization security

Definición

401 Unauthorized y 403 Forbidden son dos códigos de estado HTTP distintos que a menudo se confunden. Comprender la diferencia es crítico para la seguridad de la API y los flujos apropiados de autenticación/autorización.

401 Unauthorized:

  • Significado: La solicitud requiere autenticación (el usuario debe identificarse)
  • Razón: Credenciales de autenticación faltantes, inválidas o expiradas
  • Solución: El cliente debe proporcionar credenciales válidas (login, refrescar token)
  • Encabezado: Debe incluir encabezado WWW-Authenticate especificando el método de autenticación

403 Forbidden:

  • Significado: El servidor entendió la solicitud pero se niega a autorizarla
  • Razón: El usuario está autenticado pero carece de permiso para acceder al recurso
  • Solución: El cliente no puede arreglarlo; requiere cambio de permiso en el servidor
  • Encabezado: No se requiere encabezado WWW-Authenticate

Distinción Clave:

  • 401 → “No sé quién eres” (problema de identidad)
  • 403 → “Sé quién eres, pero no puedes hacer eso” (problema de permiso)

Ejemplo

401 Unauthorized - Token Faltante:

GET /api/admin/users HTTP/1.1
Host: api.example.com

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api.example.com"
Content-Type: application/json

{
  "error": "No autorizado",
  "message": "Autenticación requerida. Por favor, proporciona un token de acceso válido."
}

401 Unauthorized - Token Expirado:

GET /api/admin/users HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.EXPIRED_TOKEN

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Token expired"
Content-Type: application/json

{
  "error": "No autorizado",
  "message": "El token de acceso ha expirado. Por favor, refresca tu token.",
  "expiredAt": "2026-01-09T09:00:00Z"
}

403 Forbidden - Permisos Insuficientes (Usuario Regular Accediendo a Admin):

GET /api/admin/users HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.VALID_TOKEN

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": "Prohibido",
  "message": "No tienes permiso para acceder a este recurso. Se requiere rol de administrador.",
  "requiredRole": "admin",
  "userRole": "user"
}

403 Forbidden - Cuenta Suspendida:

POST /api/posts HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.VALID_BUT_SUSPENDED_USER

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": "Prohibido",
  "message": "Tu cuenta ha sido suspendida. Contacta a soporte para asistencia.",
  "suspendedAt": "2026-01-08T15:00:00Z",
  "reason": "Violación de Términos de Servicio"
}

Ejemplo de Código

JavaScript (Fetch API):

const fetchAdminUsers = async () => {
  try {
    const response = await fetch('https://api.example.com/admin/users', {
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('token')}`,
        'Accept': 'application/json'
      }
    });

    // Manejar 401 Unauthorized
    if (response.status === 401) {
      const error = await response.json();
      console.error('401 No autorizado:', error.message);

      // Verificar si el token expiró
      if (error.message.includes('expirado')) {
        console.log('Token expirado, intentando refrescar...');

        // Intentar refrescar token
        const refreshed = await refreshAccessToken();
        if (refreshed) {
          // Reintentar solicitud con nuevo token
          return fetchAdminUsers();
        }
      }

      // Redirigir al login
      console.log('Redirigiendo al login...');
      window.location.href = '/login';
      return null;
    }

    // Manejar 403 Forbidden
    if (response.status === 403) {
      const error = await response.json();
      console.error('403 Prohibido:', error.message);

      // Mostrar mensaje amigable
      alert('Acceso Denegado: ' + error.message);

      // Redirigir al dashboard (no tiene sentido reintentar)
      window.location.href = '/dashboard';
      return null;
    }

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return await response.json();

  } catch (error) {
    console.error('Error obteniendo usuarios admin:', error);
    throw error;
  }
};

const refreshAccessToken = async () => {
  try {
    const refreshToken = localStorage.getItem('refreshToken');

    const response = await fetch('https://api.example.com/auth/refresh', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ refreshToken })
    });

    if (response.status === 401) {
      // El token de refresco también expiró
      console.error('Token de refresco expirado, debe volver a iniciar sesión');
      return false;
    }

    if (response.ok) {
      const data = await response.json();
      localStorage.setItem('token', data.accessToken);
      console.log('Token de acceso refrescado exitosamente');
      return true;
    }

    return false;

  } catch (error) {
    console.error('Error refrescando token:', error);
    return false;
  }
};

Python (librería requests):

import requests

def fetch_admin_users():
    try:
        token = get_token_from_storage()

        response = requests.get(
            'https://api.example.com/admin/users',
            headers={
                'Authorization': f'Bearer {token}',
                'Accept': 'application/json'
            }
        )

        # Manejar 401 Unauthorized
        if response.status_code == 401:
            error = response.json()
            print(f'401 No autorizado: {error.get("message")}')

            # Verificar si el token expiró
            if 'expirado' in error.get('message', '').lower():
                print('Token expirado, intentando refrescar...')

                # Intentar refrescar token
                if refresh_access_token():
                    # Reintentar solicitud con nuevo token
                    return fetch_admin_users()

            # Redirigir al login
            print('Autenticación requerida, redirigiendo al login...')
            return None

        # Manejar 403 Forbidden
        if response.status_code == 403:
            error = response.json()
            print(f'403 Prohibido: {error.get("message")}')

            # Mostrar mensaje amigable
            print(f'Acceso Denegado: {error.get("message")}')

            # No tiene sentido reintentar (el permiso no cambiará)
            return None

        response.raise_for_status()

        return response.json()

    except requests.exceptions.RequestException as e:
        print(f'Error obteniendo usuarios admin: {e}')
        raise

def refresh_access_token():
    try:
        refresh_token = get_refresh_token_from_storage()

        response = requests.post(
            'https://api.example.com/auth/refresh',
            json={'refreshToken': refresh_token}
        )

        if response.status_code == 401:
            # El token de refresco también expiró
            print('Token de refresco expirado, debe volver a iniciar sesión')
            return False

        if response.ok:
            data = response.json()
            save_token_to_storage(data['accessToken'])
            print('Token de acceso refrescado exitosamente')
            return True

        return False

    except requests.exceptions.RequestException as e:
        print(f'Error refrescando token: {e}')
        return False

Diagrama

flowchart TB
    START[Solicitud API] --> HAS_TOKEN{¿Tiene Token
de Auth?} HAS_TOKEN -->|No| RETURN_401[Devolver 401
Encabezado WWW-Authenticate] HAS_TOKEN -->|Sí| VALIDATE{¿Token Válido?} VALIDATE -->|Inválido/Expirado| RETURN_401_EXPIRED[Devolver 401
Token expirado/inválido] VALIDATE -->|Válido| CHECK_PERM{¿Tiene Permiso?} CHECK_PERM -->|No| RETURN_403[Devolver 403
Permisos insuficientes] CHECK_PERM -->|Sí| RETURN_200[Devolver 200
Éxito] RETURN_401 --> CLIENT_LOGIN[Cliente: Login/Proporcionar Token] RETURN_401_EXPIRED --> CLIENT_REFRESH[Cliente: Refrescar Token] RETURN_403 --> CLIENT_REQUEST[Cliente: Solicitar Acceso
o Rendirse] RETURN_200 --> CLIENT_SUCCESS[Cliente: Procesar Respuesta] style RETURN_401 fill:#ff6b6b style RETURN_401_EXPIRED fill:#ff6b6b style RETURN_403 fill:#ffa726 style RETURN_200 fill:#66bb6a

Notas de Seguridad

SECURITY NOTES

CRÍTICO - Siempre usar 401 para fallos de autenticación y 403 para fallos de autorización.

Configuración y Validación:

  • Siempre usar 401 para fallos de autenticación y 403 para fallos de autorización.
  • Nunca intercambiar estos códigos ya que confunde a los clientes y crea vulnerabilidades de seguridad.
  • Siempre incluir encabezado WWW-Authenticate con respuestas 401.
  • Nunca exponer lógica de permisos interna en mensajes de error 403.

Monitoreo y Auditoría:

  • Registrar todas las respuestas 401 y 403 para monitoreo de seguridad.
  • Usar 403 para limitación de tasa en endpoints autenticados en lugar de 401.
  • Nunca usar 404 para ocultar recursos de usuarios no autorizados (seguridad por oscuridad).
  • Implementar flujos apropiados de expiración y refresco de tokens para minimizar errores 401.

Analogía

Piensa en 401 vs 403 como entrar a un edificio:

  • 401 Unauthorized → “Muéstrame tu tarjeta de identificación” (guardia en la entrada)
  • 403 Forbidden → “Tu tarjeta funciona, pero no puedes acceder al piso ejecutivo” (restricción del elevador)

Necesitas identificación apropiada (401) antes de que puedan verificar si tienes acceso (403).

Buenas Prácticas

  1. Usar 401 para Autenticación - Credenciales faltantes, inválidas o expiradas
  2. Usar 403 para Autorización - Credenciales válidas pero permisos insuficientes
  3. Incluir WWW-Authenticate - Siempre agregar este encabezado a respuestas 401
  4. Proporcionar Mensajes Claros - Explicar por qué se denegó el acceso
  5. Nunca Intercambiar Códigos - No usar 401 cuando quieres decir 403 o viceversa
  6. Registrar Eventos de Seguridad - Rastrear respuestas 401/403 para detección de intrusiones
  7. Implementar Refresco de Token - Usar refresh tokens para minimizar errores 401
  8. No Ocultar Recursos - Usar 403/401 apropiado, no 404 por seguridad

Errores Comunes

  • Usar 401 para Autorización - Devolver 401 cuando el usuario carece de permisos (debería ser 403)
  • Usar 403 para Autenticación - Devolver 403 cuando el token falta/expiró (debería ser 401)
  • Sin Encabezado WWW-Authenticate - No incluir encabezado requerido con respuestas 401
  • 404 por Seguridad - Ocultar recursos con 404 en lugar de 401/403 apropiado
  • Exponer Lógica de Permisos - Incluir demasiado detalle en mensajes de error 403
  • Sin Reintento para 401 - No implementar flujos de refresco de token en clientes
  • Reintentar 403 - Clientes reintentando errores 403 (que no cambiarán sin actualización de permisos del lado del servidor)

Estándares y RFCs

Términos Relacionados