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-Authenticateespecificando 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
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
- Usar 401 para Autenticación - Credenciales faltantes, inválidas o expiradas
- Usar 403 para Autorización - Credenciales válidas pero permisos insuficientes
- Incluir WWW-Authenticate - Siempre agregar este encabezado a respuestas 401
- Proporcionar Mensajes Claros - Explicar por qué se denegó el acceso
- Nunca Intercambiar Códigos - No usar 401 cuando quieres decir 403 o viceversa
- Registrar Eventos de Seguridad - Rastrear respuestas 401/403 para detección de intrusiones
- Implementar Refresco de Token - Usar refresh tokens para minimizar errores 401
- 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)