La expresión "error 403 Unauthorized" combina incorrectamente dos códigos HTTP distintos. El código 403 se llama Forbidden (prohibido) y significa que el servidor conoce la identidad del cliente y aun así le deniega el acceso. El código 401 se llama Unauthorized (no autorizado) y significa que el servidor no sabe quién es el cliente y necesita que se identifique. La confusión es muy frecuente y tiene una razón histórica: el RFC original de HTTP usó el término "Unauthorized" para el 401 cuando en realidad debería haberse llamado "Unauthenticated". Esta distinción no es solo semántica: determina completamente cómo resolver el problema de acceso.

El origen de la confusión: un error histórico del estándar HTTP

La confusión entre 401 y 403 tiene una explicación histórica. El estándar HTTP/1.0, publicado en 1996, definió el código 401 como "Unauthorized" en un momento en que la terminología de seguridad informática no estaba tan consolidada como hoy. En el lenguaje cotidiano, "unauthorized" puede significar tanto "no autenticado" (no sabemos quién eres) como "no autorizado" (sabemos quién eres pero no tienes permiso). El RFC eligió el término más ambiguo para el 401.

La consecuencia es que el estándar HTTP usa los términos de forma contraintuitiva:

  • El código 401 se llama "Unauthorized" pero significa no autenticado: el problema es de identidad, no de permisos.
  • El código 403 se llama "Forbidden" pero es el que realmente implementa la autorización: el servidor conoce la identidad del cliente y le deniega el acceso por falta de permisos.

El RFC 9110 (2022), que es el estándar HTTP vigente, reconoce explícitamente este problema terminológico y aclara en sus notas que el nombre "Unauthorized" para el 401 es históricamente inapropiado y que debería haberse llamado "Unauthenticated".

Autenticación vs autorización: la distinción fundamental

Para entender correctamente los códigos 401 y 403, es imprescindible entender la diferencia entre autenticación y autorización. Son dos conceptos de seguridad distintos que se aplican de forma secuencial:

ConceptoPregunta que respondeCódigo HTTP cuando fallaEjemplo
Autenticación (Authentication)¿Quién eres? ¿Puedo verificar tu identidad?401 UnauthorizedIntroduces tu usuario y contraseña para demostrar quién eres
Autorización (Authorization)¿Tienes permiso para hacer esto? ¿Tu rol permite esta acción?403 ForbiddenEl sistema comprueba si tu rol (usuario, editor, admin) tiene acceso al recurso solicitado

El flujo correcto en cualquier sistema con control de acceso es siempre en este orden:

  1. Autenticación primero: el sistema verifica que eres quien dices ser (login con usuario y contraseña, token JWT, API key, certificado de cliente).
  2. Autorización después: una vez verificada tu identidad, el sistema comprueba si tu cuenta, rol o grupo tiene permiso para acceder al recurso o ejecutar la acción solicitada.

Si falla el paso 1 (no te has identificado o tus credenciales son incorrectas): 401.
Si falla el paso 2 (te has identificado correctamente pero tu rol no tiene acceso): 403.

Definición exacta de cada código según el RFC 9110

Código 401 Unauthorized

El RFC 9110 define el 401 así: "The request has not been applied because it lacks valid authentication credentials for the target resource." (La solicitud no se ha procesado porque carece de credenciales de autenticación válidas para el recurso de destino).

Características técnicas del 401:

  • El servidor debe incluir la cabecera WWW-Authenticate en la respuesta, indicando el esquema de autenticación requerido (Basic, Bearer, Digest...).
  • Implica que el cliente puede resolver el problema enviando las credenciales correctas.
  • Si el cliente ya envió credenciales y el servidor devuelve 401, significa que esas credenciales son incorrectas o han expirado.
# Respuesta HTTP 401 típica:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api.tudominio.com"
Content-Type: application/json

{
  "error": "authentication_required",
  "message": "Se requiere autenticación. Incluye un token Bearer válido."
}

Código 403 Forbidden

El RFC 9110 define el 403 así: "The server understood the request but refuses to fulfill it. If authentication credentials were provided in the request, the server considers them insufficient to grant access." (El servidor entendió la solicitud pero se niega a cumplirla. Si se proporcionaron credenciales de autenticación, el servidor las considera insuficientes para conceder acceso).

Características técnicas del 403:

  • El servidor no incluye la cabecera WWW-Authenticate porque no es un problema de autenticación.
  • Implica que el cliente no puede resolver el problema enviando credenciales: el problema es de autorización, no de identidad.
  • El acceso está denegado independientemente de quién sea el cliente.
  • El servidor puede elegir no revelar si el recurso existe (para no dar pistas a atacantes).
# Respuesta HTTP 403 típica:
HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": "insufficient_permissions",
  "message": "Tu rol no tiene acceso a este recurso. Contacta con el administrador."
}

Tabla comparativa completa: 401 vs 403

Dimensión401 Unauthorized403 Forbidden
Nombre oficial (RFC 9110)UnauthorizedForbidden
Significado realNo autenticado: el servidor no sabe quién eresNo autorizado: el servidor sabe quién eres y te deniega el acceso
ProblemaFalta de autenticación o credenciales incorrectasFalta de autorización o permisos insuficientes
Cabecera WWW-AuthenticateObligatoria: indica el método de autenticación requeridoNo se incluye: no es un problema de autenticación
¿El cliente puede resolverlo?Sí: enviando las credenciales correctasNo desde el cliente: el administrador debe cambiar los permisos
¿El recurso existe?Puede existir o no; el servidor no lo revela hasta la autenticaciónPuede existir o no; el servidor puede ocultarlo deliberadamente
Cabecera de la respuestaWWW-Authenticate: Basic realm="ejemplo", Bearer realm="api"Sin WWW-Authenticate
Causa típica en servidor LinuxSesión caducada, token expirado, .htpasswd con contraseña incorrectaPermisos chmod incorrectos, .htaccess con Deny, IP bloqueada
Causa típica en API RESTToken ausente, token inválido, token expiradoToken válido pero rol sin permiso, recurso de otro usuario
Analogía físicaIntentas entrar a un edificio sin mostrar tu identificaciónMuestras tu identificación pero no tienes acceso a esa planta

Cuándo usar 401 y cuándo usar 403 en una API REST

El diseño correcto de los códigos de respuesta en APIs REST es fundamental para que los clientes (aplicaciones móviles, frontends, integraciones) puedan manejar los errores de forma automática y mostrar el mensaje adecuado al usuario. Un error frecuente en el desarrollo de APIs es usar 403 cuando debería ser 401 o viceversa, lo que confunde a los clientes sobre cómo resolver el problema.

Escenarios donde corresponde el código 401

SituaciónCódigo correctoMotivo
La solicitud no incluye ningún token de autorización en la cabecera401El servidor no puede verificar la identidad del cliente: no sabe quién es
El token de autorización incluido está malformado o no es válido401El servidor no puede parsear o verificar el token: la autenticación falla
El token de autorización ha expirado (JWT expirado, sesión caducada)401Las credenciales eran válidas pero ya no lo son: necesita renovarlas
La API key no existe en la base de datos401Las credenciales no corresponden a ninguna cuenta conocida
El usuario y contraseña en una autenticación Basic son incorrectos401La autenticación falla: las credenciales no verifican la identidad

Escenarios donde corresponde el código 403

SituaciónCódigo correctoMotivo
El token es válido pero el usuario tiene rol "viewer" e intenta ejecutar una acción de escritura403La identidad está verificada pero el rol no tiene ese permiso
El usuario autenticado intenta acceder al recurso de otro usuario (GET /users/456/datos siendo el usuario 123)403El usuario está autenticado pero no tiene permiso sobre ese recurso específico
Un usuario con cuenta activa intenta acceder a una sección de pago sin haber pagado403La identidad es válida pero la cuenta no tiene los privilegios requeridos
Se intenta ejecutar una operación en un recurso que está en estado de solo lectura403El recurso existe y el usuario está autenticado, pero la operación no está permitida en ese estado
Un usuario bloqueado o suspendido intenta usar la API con credenciales técnicamente válidas403Las credenciales son válidas (autenticación OK) pero la cuenta no tiene acceso (autorización denegada)

El caso especial: ¿401 o 403 cuando el recurso no existe?

Este es uno de los debates más interesantes en el diseño de APIs seguras. Imagina que un usuario autenticado solicita GET /documentos/secreto-confidencial. El documento existe pero el usuario no tiene acceso. ¿Qué código devolver?

  • 403 Forbidden: correcto técnicamente, pero revela al cliente que el recurso existe.
  • 404 Not Found: oculta la existencia del recurso, pero puede confundir a clientes legítimos.

La práctica recomendada en seguridad es devolver 404 cuando revelar la existencia del recurso supone un riesgo de seguridad (documentos confidenciales, datos de otros usuarios, endpoints administrativos). El principio es que un usuario sin acceso no debería poder confirmar si un recurso existe o no. Cuando la existencia del recurso no es información sensible, usar 403 es más correcto semánticamente.

Implementación correcta en código

En PHP (WordPress, Laravel, aplicaciones personalizadas)

<?php

// Función helper para respuestas de error de autenticación/autorización:

function respondUnauthorized(string $message = 'Autenticación requerida'): void {
    // 401: el cliente no está autenticado
    http_response_code(401);
    header('WWW-Authenticate: Bearer realm="api.tudominio.com"');
    header('Content-Type: application/json');
    echo json_encode([
        'error'   => 'authentication_required',
        'message' => $message,
        'code'    => 401
    ]);
    exit;
}

function respondForbidden(string $message = 'Acceso denegado'): void {
    // 403: el cliente está autenticado pero no tiene permiso
    http_response_code(403);
    header('Content-Type: application/json');
    echo json_encode([
        'error'   => 'insufficient_permissions',
        'message' => $message,
        'code'    => 403
    ]);
    exit;
}

// Uso correcto en un endpoint protegido:
function getAdminDashboard(): void {
    $token = getBearerToken();

    if (!$token) {
        // No hay token: no sabemos quién es el cliente → 401
        respondUnauthorized('Token de autenticación requerido');
    }

    $user = validateToken($token);

    if (!$user) {
        // Token inválido o expirado: autenticación fallida → 401
        respondUnauthorized('Token inválido o expirado');
    }

    if ($user->role !== 'admin') {
        // El usuario está autenticado pero no tiene rol admin → 403
        respondForbidden('Se requiere rol de administrador para acceder a este recurso');
    }

    // El usuario está autenticado Y tiene el rol correcto: procesar la solicitud
    renderAdminDashboard($user);
}
?>

En Node.js / Express

// Middleware de autenticación — devuelve 401 si no hay token o es inválido:
const authenticate = async (req, res, next) => {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
        // No hay token: no sabemos quién es → 401
        return res.status(401)
            .set('WWW-Authenticate', 'Bearer realm="api.tudominio.com"')
            .json({
                error: 'authentication_required',
                message: 'Token de autenticación requerido en la cabecera Authorization'
            });
    }

    const token = authHeader.split(' ')[1];

    try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.user = decoded;
        next();
    } catch (error) {
        if (error.name === 'TokenExpiredError') {
            // Token expirado: credenciales caducadas → 401
            return res.status(401)
                .set('WWW-Authenticate', 'Bearer realm="api.tudominio.com", error="invalid_token"')
                .json({
                    error: 'token_expired',
                    message: 'El token ha expirado. Obtén uno nuevo.'
                });
        }
        // Token inválido: autenticación fallida → 401
        return res.status(401).json({
            error: 'invalid_token',
            message: 'Token inválido'
        });
    }
};

// Middleware de autorización — devuelve 403 si el rol es insuficiente:
const authorize = (...allowedRoles) => {
    return (req, res, next) => {
        // Si llegamos aquí, el usuario ya está autenticado (authenticate pasó)
        if (!allowedRoles.includes(req.user.role)) {
            // El usuario está autenticado pero su rol no tiene acceso → 403
            return res.status(403).json({
                error: 'insufficient_permissions',
                message: `Se requiere uno de los siguientes roles: ${allowedRoles.join(', ')}. Tu rol actual es: ${req.user.role}`
            });
        }
        next();
    };
};

// Uso en las rutas:
app.get('/api/admin/usuarios',
    authenticate,                    // Primero: verificar identidad (401 si falla)
    authorize('admin', 'superadmin'), // Después: verificar rol (403 si falla)
    adminController.getUsuarios       // Solo llega aquí si pasa ambos
);

app.delete('/api/admin/usuario/:id',
    authenticate,
    authorize('admin'),
    adminController.deleteUsuario
);

En Python (Django / Flask)

# Django REST Framework — uso correcto de excepciones de autenticación:
from rest_framework.exceptions import AuthenticationFailed, PermissionDenied
from rest_framework.permissions import BasePermission

class IsTokenValid(BasePermission):
    """
    Verifica que el token es válido.
    Lanza 401 si el token falta o es inválido (problema de autenticación).
    """
    def has_permission(self, request, view):
        token = request.META.get('HTTP_AUTHORIZATION', '').split(' ')[-1]

        if not token:
            # Sin token: 401 Unauthorized
            raise AuthenticationFailed('Token de autenticación requerido')

        user = verify_token(token)
        if not user:
            # Token inválido: 401 Unauthorized
            raise AuthenticationFailed('Token inválido o expirado')

        request.user = user
        return True


class IsAdmin(BasePermission):
    """
    Verifica que el usuario autenticado tiene rol de administrador.
    Lanza 403 si el rol es insuficiente (problema de autorización).
    """
    def has_permission(self, request, view):
        if request.user.role != 'admin':
            # Usuario autenticado pero sin rol admin: 403 Forbidden
            raise PermissionDenied('Se requiere rol de administrador')
        return True


# En la vista:
class AdminDashboardView(APIView):
    permission_classes = [IsTokenValid, IsAdmin]  # Aplica en orden

    def get(self, request):
        # Solo llega aquí si IsTokenValid e IsAdmin pasan sin excepciones
        return Response({'data': get_admin_data()})

Errores de diseño frecuentes: cuándo los desarrolladores confunden 401 y 403

Error 1: Devolver 403 cuando el usuario no está autenticado

Muchos desarrolladores devuelven 403 para todos los casos de acceso denegado, sin distinguir si el problema es de autenticación o de autorización. El resultado es que el cliente no sabe si debe pedir al usuario que inicie sesión (problema de autenticación, 401) o mostrar un mensaje de "no tienes permiso" (problema de autorización, 403).

Incorrecto:

// Devuelve 403 para TODOS los casos de acceso denegado:
if (!request.user) {
    return res.status(403).json({ error: 'Acceso denegado' }); // MAL: debería ser 401
}
if (request.user.role !== 'admin') {
    return res.status(403).json({ error: 'Acceso denegado' }); // BIEN: aquí sí es 403
}

Correcto:

// Diferencia correctamente entre autenticación y autorización:
if (!request.user) {
    return res.status(401)                               // BIEN: problema de autenticación
        .set('WWW-Authenticate', 'Bearer realm="api"')
        .json({ error: 'authentication_required' });
}
if (request.user.role !== 'admin') {
    return res.status(403).json({                        // BIEN: problema de autorización
        error: 'insufficient_permissions'
    });
}

Error 2: Devolver 401 cuando el usuario está autenticado pero no tiene permiso

El error inverso: devolver 401 cuando el usuario ya está autenticado pero su rol no permite la acción. Esto confunde al cliente, que intentará volver a autenticarse sin éxito porque el problema no es de autenticación.

Incorrecto:

// El usuario está autenticado pero no tiene rol de admin:
if (user.role !== 'admin') {
    return res.status(401).json({ error: 'No autorizado' }); // MAL: debería ser 403
    // El cliente intentará renovar el token sin éxito porque el token es válido
}

Error 3: No incluir la cabecera WWW-Authenticate en el 401

El RFC 9110 establece que una respuesta 401 debe incluir la cabecera WWW-Authenticate indicando el esquema de autenticación requerido. Sin esta cabecera, los clientes HTTP estándar no saben cómo autenticarse y algunos navegadores no mostrarán el diálogo de autenticación.

Incorrecto:

HTTP/1.1 401 Unauthorized
Content-Type: application/json
// Falta la cabecera WWW-Authenticate

Correcto:

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

Cómo afectan al usuario final: mensajes de error apropiados

Aunque el usuario final no ve los códigos HTTP directamente, la aplicación debería usar el código correcto para mostrar el mensaje adecuado:

Código recibidoMensaje apropiado para el usuarioAcción que debe ofrecerse
401"Tu sesión ha expirado" o "Debes iniciar sesión para acceder"Botón de inicio de sesión o renovación de token automática
403"No tienes permiso para acceder a esta sección" o "Esta función requiere una cuenta de administrador"Información de contacto con el administrador; no ofrecer botón de login (no sirve)

Una aplicación que recibe un 401 y muestra "No tienes permiso" (mensaje de 403) está engañando al usuario: tiene permiso, pero necesita autenticarse. Una aplicación que recibe un 403 y redirige al login está despistando al usuario: iniciar sesión no resolverá el problema.

Impacto en el SEO

Desde la perspectiva de Googlebot, la distinción entre 401 y 403 tiene implicaciones concretas:

  • 401 en URLs públicas: Googlebot no intenta autenticarse (no tiene credenciales). Recibe el 401 y no indexa el contenido. Si persiste, puede desindexar URLs con historial de posicionamiento.
  • 403 en URLs públicas: Googlebot recibe el 403 y tampoco puede indexar el contenido. El impacto SEO es equivalente al del 401.
  • 401 y 403 en URLs administrativas: sin impacto en el SEO. Googlebot entiende que esas URLs no son contenido público indexable.

Google Search Console muestra ambos tipos de error en la sección Cobertura y los clasifica simplemente como "error del servidor". Verifica regularmente que ninguna URL pública esté devolviendo 401 o 403 de forma no intencionada.

Preguntas frecuentes sobre la diferencia entre 401 y 403

¿Por qué el código 401 se llama "Unauthorized" si lo que significa es "no autenticado"?

Es un error histórico del estándar HTTP. El RFC 2616 de 1999 usó el término "Unauthorized" para el código 401 en una época en que la terminología de seguridad no estaba tan diferenciada como hoy. En inglés, "unauthorized" puede significar tanto "no autenticado" como "no autorizado", y el RFC eligió el término ambiguo. El RFC 9110 de 2022, que es el estándar vigente, reconoce explícitamente en sus notas que el nombre "Unauthorized" es históricamente inapropiado para el 401 y que debería haberse llamado "Unauthenticated".

¿Qué código HTTP debo devolver si un usuario autenticado intenta acceder al perfil de otro usuario?

El código correcto es 403 Forbidden. El usuario está autenticado correctamente (su identidad está verificada), pero no tiene permiso para acceder al recurso de otro usuario. Algunos sistemas de seguridad devuelven 404 Not Found en este caso para no revelar que el recurso existe, lo que puede ser más seguro en escenarios donde la existencia del perfil es información sensible. La elección entre 403 y 404 en este caso depende de los requisitos de privacidad del sistema.

¿Si el token JWT ha expirado, debo devolver 401 o 403?

Debes devolver 401 Unauthorized. Un token JWT expirado es un problema de autenticación: las credenciales que el cliente presentó ya no son válidas. El cliente debe obtener un nuevo token (por ejemplo, usando el refresh token) y reintentar la solicitud. Si devolvieras 403, el cliente podría interpretar que necesita solicitar permisos adicionales, cuando en realidad solo necesita renovar sus credenciales.

¿Cuándo tiene sentido devolver 404 en lugar de 403?

Tiene sentido devolver 404 en lugar de 403 cuando revelar que el recurso existe supone un riesgo de seguridad o privacidad. Por ejemplo: si un usuario intenta acceder a GET /usuarios/456/datos y ese usuario existe pero el solicitante no tiene permiso, devolver 403 confirma que el usuario 456 existe en el sistema. Si eso es información sensible (en un sistema de mensajería anónima, por ejemplo), es preferible devolver 404 para no revelar la existencia del recurso. En la mayoría de los sistemas de uso general, 403 es la respuesta más correcta semánticamente.

¿Es incorrecto usar siempre 403 para simplificar el diseño de la API?

Técnicamente es incorrecto según el RFC 9110, pero es una práctica frecuente en APIs pequeñas o por razones de seguridad (no revelar información sobre el estado de autenticación). El problema práctico es que los clientes que siguen el estándar no sabrán si deben pedir al usuario que se autentique (401) o mostrar un mensaje de permisos insuficientes (403). Si tu API va a ser consumida por terceros o por clientes estándar, el uso correcto de 401 y 403 es importante para la interoperabilidad.

¿Cómo puedo comprobar qué código HTTP devuelve un endpoint?

La forma más directa es usar curl desde la línea de comandos, que muestra el código de respuesta sin seguir redirecciones automáticamente:

# Ver solo el código de estado HTTP:
curl -o /dev/null -s -w "%{http_code}\n" https://api.tudominio.com/admin/datos

# Ver las cabeceras completas de la respuesta (útil para ver WWW-Authenticate):
curl -I https://api.tudominio.com/admin/datos

# Ver cabeceras y cuerpo de la respuesta con un token:
curl -H "Authorization: Bearer tu-token-aqui" \
     -v https://api.tudominio.com/admin/datos 2>&1 | grep -E "< HTTP|< WWW|{.*}"