Qué es la autenticación vs la autorización: diferencias y ejemplos

D
DanisCh
(Actualizado: ) • 15 min de lectura
Qué es la autenticación vs la autorización: diferencias y ejemplos
Cursos recomendados (Ciberseguridad) Programación

Autenticación y autorización son dos conceptos de seguridad que se confunden constantemente, incluso en documentación técnica. Se usan juntos siempre, se complementan y guardan una relación de orden: primero se autentica, luego se autoriza. Pero son dos preguntas completamente distintas.

La autenticación responde a: ¿quién eres? Verifica la identidad.

La autorización responde a: ¿qué puedes hacer? Verifica los permisos.

En esta guía aprenderás cómo funciona cada una, cuáles son los mecanismos más habituales y cómo implementar ambas en una aplicación web real.

La diferencia con un ejemplo cotidiano

Imagina que llegas al trabajo a un edificio de oficinas:

  • En la entrada muestras tu tarjeta de empleado. El guardia verifica que eres quien dices ser. Eso es autenticación.
  • Una vez dentro, tu tarjeta abre la puerta de tu planta pero no la del servidor room ni la del despacho del director. Tu identidad determina a qué espacios tienes acceso. Eso es autorización.

En una aplicación web:

  • Introduces tu email y contraseña y el sistema te reconoce. Autenticación.
  • Puedes ver tus propios pedidos pero no los de otros usuarios, y no puedes acceder al panel de administración. Autorización.

El orden importa: no puedes autorizar a alguien que no has autenticado primero. Si el sistema no sabe quién eres, no puede decidir qué puedes hacer.

Autenticación: verificar la identidad

La autenticación prueba que el usuario es quien dice ser. Hay tres grandes categorías de factores de autenticación:

  • Algo que sabes: contraseña, PIN, pregunta de seguridad.
  • Algo que tienes: teléfono móvil, token físico, tarjeta inteligente.
  • Algo que eres: huella dactilar, reconocimiento facial, iris.

La autenticación multifactor (MFA) combina dos o más categorías, lo que hace mucho más difícil que alguien suplante tu identidad: aunque robe tu contraseña, necesitaría también tu teléfono.

Autenticación con usuario y contraseña

Es el mecanismo más habitual. El usuario proporciona credenciales, el servidor las verifica y devuelve un token que el cliente usa en peticiones posteriores.

# Python/FastAPI: endpoint de login
from fastapi import FastAPI, HTTPException, Depends
from passlib.context import CryptContext
import jwt
from datetime import datetime, timedelta

app = FastAPI()
pwd_context = CryptContext(schemes=["bcrypt"])
SECRET_KEY = "tu-clave-secreta-muy-larga"   # en producción, desde variable de entorno

# Base de datos simulada
usuarios_db = {
    "ana@mail.com": {
        "id": 1,
        "nombre": "Ana García",
        "email": "ana@mail.com",
        # La contraseña NUNCA se guarda en texto plano: siempre hasheada
        "contraseña_hash": pwd_context.hash("mi-contraseña-segura"),
        "rol": "usuario"
    }
}

def verificar_contraseña(contraseña_plana: str, hash_guardado: str) -> bool:
    return pwd_context.verify(contraseña_plana, hash_guardado)

def crear_token(datos: dict, expira_en_minutos: int = 60) -> str:
    payload = datos.copy()
    payload["exp"] = datetime.utcnow() + timedelta(minutes=expira_en_minutos)
    return jwt.encode(payload, SECRET_KEY, algorithm="HS256")

@app.post("/auth/login")
def login(email: str, contraseña: str):
    usuario = usuarios_db.get(email)

    # Verificar que el usuario existe Y que la contraseña es correcta
    # El mensaje de error es genérico: no revelar si el email existe o no
    if not usuario or not verificar_contraseña(contraseña, usuario["contraseña_hash"]):
        raise HTTPException(
            status_code=401,
            detail="Email o contraseña incorrectos"
        )

    token = crear_token({
        "sub":    str(usuario["id"]),   # subject: quién es el usuario
        "email":  usuario["email"],
        "rol":    usuario["rol"],
    })

    return {
        "token":    token,
        "tipo":     "Bearer",
        "expira_en": 3600
    }
// Node.js/Express: mismo flujo en JavaScript
const express    = require('express');
const bcrypt     = require('bcrypt');
const jwt        = require('jsonwebtoken');
const app        = express();
app.use(express.json());
const SECRET_KEY = process.env.JWT_SECRET;   // siempre desde variable de entorno
// Simulación de base de datos
const usuariosDB = {
 'ana@mail.com': {
   id:               1,
   nombre:           'Ana García',
   email:            'ana@mail.com',
   contraseñaHash:   await bcrypt.hash('mi-contraseña', 12),
   rol:              'usuario'
 }
};
app.post('/auth/login', async (req, res) => {
 const { email, contraseña } = req.body;
 const usuario = usuariosDB[email];
 // bcrypt.compare compara el texto con el hash sin exponer el hash
 const credencialesValidas = usuario && await bcrypt.compare(contraseña, usuario.contraseñaHash);
 if (!credencialesValidas) {
   return res.status(401).json({ error: 'Email o contraseña incorrectos' });
 }
 const token = jwt.sign(
   { sub: usuario.id, email: usuario.email, rol: usuario.rol },
   SECRET_KEY,
   { expiresIn: '1h' }
 );
 res.json({ token, tipo: 'Bearer', expira_en: 3600 });
});

Cómo se almacenan las contraseñas de forma segura

# NUNCA guardar contraseñas en texto plano
# NUNCA usar MD5 o SHA1 para contraseñas (son rápidos y fáciles de romper por fuerza bruta)
# SIEMPRE usar bcrypt, Argon2 o scrypt: son lentos por diseño, lo que dificulta los ataques

import bcrypt

# Registro: hashear antes de guardar
contraseña_plana = "mi-contraseña-segura"
salt_rounds = 12    # coste computacional: más alto = más seguro pero más lento

hash = bcrypt.hashpw(contraseña_plana.encode(), bcrypt.gensalt(salt_rounds))
# guardar el hash en la base de datos, nunca la contraseña original

# Login: verificar sin revelar el hash
contraseña_introducida = "mi-contraseña-segura"
es_valida = bcrypt.checkpw(contraseña_introducida.encode(), hash)
# True si coincide, False si no

# El hash incluye el salt automáticamente, así dos hashes del mismo texto son distintos:
hash1 = bcrypt.hashpw(b"misma", bcrypt.gensalt())
hash2 = bcrypt.hashpw(b"misma", bcrypt.gensalt())
print(hash1 == hash2)   # False (por el salt aleatorio)

Tokens JWT: cómo funcionan

Un JWT (JSON Web Token) es un token firmado que contiene información sobre el usuario. El servidor lo genera al autenticar y el cliente lo envía en cada petición. El servidor puede verificar que el token es válido sin consultar la base de datos.

# Estructura de un JWT: tres partes separadas por puntos
# eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJlbWFpbCI6ImFuYUBtYWlsLmNvbSJ9.abc123
# Parte 1 — Header (base64): algoritmo de firma
# { "alg": "HS256", "typ": "JWT" }
# Parte 2 — Payload (base64): datos del usuario (claims)
# {
#   "sub":   "123",                   # subject: id del usuario
#   "email": "ana@mail.com",
#   "rol":   "usuario",
#   "iat":   1725523200,              # issued at: cuándo se creó
#   "exp":   1725526800               # expiration: cuándo expira
# }
# Parte 3 — Signature: firma HMAC del header + payload con el secret
# Garantiza que el token no fue modificado
# IMPORTANTE: el payload NO está cifrado, solo codificado en base64
# Cualquiera puede leer el contenido del token decodificándolo
# No incluyas datos sensibles (contraseñas, datos bancarios) en el payload
# Lo que sí está garantizado es que el token no fue MODIFICADO (la firma lo certifica)
# Verificar un JWT en cada petición protegida
import jwt
def obtener_usuario_actual(token: str) -> dict:
   try:
       payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
       return payload
   except jwt.ExpiredSignatureError:
       raise HTTPException(status_code=401, detail="El token ha expirado")
   except jwt.InvalidTokenError:
       raise HTTPException(status_code=401, detail="Token inválido")
# Middleware/dependencia que extrae el token de la cabecera Authorization
from fastapi import Header
def token_requerido(authorization: str = Header(...)):
   if not authorization.startswith("Bearer "):
       raise HTTPException(status_code=401, detail="Formato de token inválido")
   token = authorization.replace("Bearer ", "")
   return obtener_usuario_actual(token)
# Proteger un endpoint con autenticación
@app.get("/perfil")
def ver_perfil(usuario_actual = Depends(token_requerido)):
   return {
       "id":     usuario_actual["sub"],
       "email":  usuario_actual["email"],
   }

Autorización: verificar los permisos

Una vez que sabemos quién es el usuario, la autorización decide qué puede hacer. Hay dos modelos principales:

RBAC: Control de acceso basado en roles

RBAC (Role-Based Access Control) asigna permisos a roles, y roles a usuarios. Es el modelo más habitual en aplicaciones web: admin, editor, usuario, moderador.

# Roles y sus permisos
PERMISOS = {
   "admin":   {"leer", "crear", "actualizar", "eliminar", "gestionar_usuarios"},
   "editor":  {"leer", "crear", "actualizar"},
   "usuario": {"leer"},
   "invitado":{"leer"}   # solo recursos públicos
}
def tiene_permiso(rol: str, permiso: str) -> bool:
   return permiso in PERMISOS.get(rol, set())
# Verificar permisos en endpoints
@app.delete("/usuarios/{id}")
def eliminar_usuario(id: int, usuario_actual = Depends(token_requerido)):
   if not tiene_permiso(usuario_actual["rol"], "gestionar_usuarios"):
       raise HTTPException(
           status_code=403,
           detail="No tienes permisos para eliminar usuarios"
       )
   # proceder con la eliminación...
   return {"mensaje": "Usuario eliminado"}
@app.post("/articulos")
def crear_articulo(datos: dict, usuario_actual = Depends(token_requerido)):
   if not tiene_permiso(usuario_actual["rol"], "crear"):
       raise HTTPException(status_code=403, detail="Sin permisos para crear artículos")
   # proceder con la creación...
# Decorador/middleware para proteger rutas por rol en Node.js/Express
function requiereRol(...rolesPermitidos) {
 return (req, res, next) => {
   const token = req.headers.authorization?.replace('Bearer ', '');
   if (!token) {
     return res.status(401).json({ error: 'Autenticación requerida' });
   }
   try {
     const usuario = jwt.verify(token, SECRET_KEY);
     req.usuario = usuario;   // añadir al request para usarlo después
     if (!rolesPermitidos.includes(usuario.rol)) {
       return res.status(403).json({ error: 'No tienes permisos para esta acción' });
     }
     next();   // pasar al siguiente middleware o al handler
   } catch (error) {
     return res.status(401).json({ error: 'Token inválido o expirado' });
   }
 };
}
// Uso en las rutas
app.get('/admin/usuarios',
 requiereRol('admin'),           // solo admins
 (req, res) => { /* ... */ }
);
app.post('/articulos',
 requiereRol('admin', 'editor'), // admins o editores
 (req, res) => { /* ... */ }
);
app.get('/perfil',
 requiereRol('admin', 'editor', 'usuario'), // cualquier usuario autenticado
 (req, res) => { /* ... */ }
);

Autorización a nivel de recurso: no solo el rol importa

El rol determina qué tipo de operaciones puede hacer un usuario, pero también hay que verificar que el usuario tiene acceso al recurso específico. Un usuario con rol "usuario" puede editar su propio perfil pero no el de otro usuario.

# Verificación combinada: rol + propiedad del recurso
@app.patch("/pedidos/{pedido_id}")
def actualizar_pedido(
   pedido_id: int,
   datos: dict,
   usuario_actual = Depends(token_requerido)
):
   pedido = db.obtener_pedido(pedido_id)
   if not pedido:
       raise HTTPException(status_code=404, detail="Pedido no encontrado")
   # Un admin puede modificar cualquier pedido
   # Un usuario solo puede modificar sus propios pedidos
   es_admin       = usuario_actual["rol"] == "admin"
   es_propietario = str(pedido.usuario_id) == usuario_actual["sub"]
   if not es_admin and not es_propietario:
       # 403 si sabemos que existe pero no tiene acceso
       # 404 si queremos ocultar que el recurso existe (más seguro)
       raise HTTPException(status_code=403, detail="No tienes acceso a este pedido")
   return db.actualizar_pedido(pedido_id, datos)
# Ejemplo completo: sistema de blog con múltiples niveles de autorización
@app.delete("/articulos/{articulo_id}")
def eliminar_articulo(
   articulo_id: int,
   usuario_actual = Depends(token_requerido)
):
   articulo = db.obtener_articulo(articulo_id)
   if not articulo:
       raise HTTPException(status_code=404)
   rol         = usuario_actual["rol"]
   usuario_id  = usuario_actual["sub"]
   # Lógica de autorización en capas:
   # 1. Los admins pueden eliminar cualquier artículo
   if rol == "admin":
       db.eliminar_articulo(articulo_id)
       return {"mensaje": "Artículo eliminado"}
   # 2. Los editores solo pueden eliminar sus propios artículos
   if rol == "editor" and str(articulo.autor_id) == usuario_id:
       db.eliminar_articulo(articulo_id)
       return {"mensaje": "Artículo eliminado"}
   # 3. Los usuarios normales no pueden eliminar artículos
   raise HTTPException(
       status_code=403,
       detail="No tienes permisos para eliminar este artículo"
   )

ABAC: Control de acceso basado en atributos

ABAC (Attribute-Based Access Control) es más flexible que RBAC: las decisiones de acceso se basan en atributos del usuario, del recurso y del contexto. Es más complejo pero permite políticas más granulares.

# ABAC: la decisión depende de múltiples atributos
def puede_ver_documento(usuario: dict, documento: dict, contexto: dict) -> bool:
   # Atributos del usuario
   es_admin        = usuario["rol"] == "admin"
   departamento    = usuario["departamento"]
   nivel_seguridad = usuario["nivel_seguridad"]
   # Atributos del documento
   doc_departamento     = documento["departamento"]
   doc_nivel_seguridad  = documento["nivel_seguridad"]
   doc_propietario      = documento["propietario_id"]
   es_publico           = documento["publico"]
   # Atributos del contexto
   es_horario_laboral = 9 <= contexto["hora"] <= 18
   es_red_corporativa = contexto["ip"].startswith("192.168.")
   # Política de acceso:
   if es_admin:
       return True   # admins pueden ver todo
   if es_publico:
       return True   # documentos públicos: todos pueden verlos
   if str(doc_propietario) == str(usuario["id"]):
       return True   # el propietario siempre puede ver el suyo
   # Mismo departamento y nivel de seguridad suficiente
   if (departamento == doc_departamento and
       nivel_seguridad >= doc_nivel_seguridad and
       es_horario_laboral and
       es_red_corporativa):
       return True
   return False

Errores de seguridad comunes relacionados con autenticación y autorización

# ─── Errores de autenticación ─────────────────────────────────────────────────
# ❌ Revelar si el email existe en el mensaje de error
if not usuario:
   raise HTTPException(detail="Email no registrado")      # revela que el email no existe
if not contraseña_valida:
   raise HTTPException(detail="Contraseña incorrecta")    # confirma que el email sí existe
# ✅ Mensaje genérico siempre
raise HTTPException(detail="Email o contraseña incorrectos")

# ❌ Guardar contraseñas en texto plano o con hash débil
usuario.contraseña = contraseña                   # texto plano
usuario.contraseña = hashlib.md5(contraseña)     # MD5: roto, no usar para contraseñas
# ✅ bcrypt, Argon2 o scrypt con coste apropiado
usuario.contraseña_hash = bcrypt.hashpw(contraseña.encode(), bcrypt.gensalt(12))

# ❌ Secret JWT débil o hardcoded en el código
SECRET_KEY = "123456"
SECRET_KEY = "mi_secret"   # valores predecibles o en el repositorio
# ✅ Secret largo, aleatorio, desde variable de entorno
# $ python -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY = os.environ["JWT_SECRET"]   # desde .env, nunca en el código

# ─── Errores de autorización ───────────────────────────────────────────────────
# ❌ Confiar en datos del cliente para la autorización
@app.delete("/pedidos/{id}")
def eliminar(id: int, rol: str = Query(...)):   # el rol viene del query param
   if rol == "admin":
       # El usuario puede simplemente enviar ?rol=admin
       db.eliminar(id)
# ✅ Siempre obtener el rol del token verificado, nunca del cliente
@app.delete("/pedidos/{id}")
def eliminar(id: int, usuario = Depends(token_requerido)):
   if usuario["rol"] != "admin":
       raise HTTPException(status_code=403)
   db.eliminar(id)

# ❌ Solo verificar autenticación, olvidarse de la autorización a nivel de recurso
@app.get("/pedidos/{id}")
def ver_pedido(id: int, usuario = Depends(token_requerido)):
   # Cualquier usuario autenticado puede ver cualquier pedido
   return db.obtener_pedido(id)   # IDOR: Insecure Direct Object Reference
# ✅ Verificar que el usuario tiene acceso a ESE recurso concreto
@app.get("/pedidos/{id}")
def ver_pedido(id: int, usuario = Depends(token_requerido)):
   pedido = db.obtener_pedido(id)
   if not pedido or (str(pedido.usuario_id) != usuario["sub"] and usuario["rol"] != "admin"):
       raise HTTPException(status_code=404)   # 404 para no revelar que existe
   return pedido

Tokens de refresco y expiración

# Un token JWT de acceso expira pronto (15 min - 1 hora)
# para limitar el daño si es robado.
# El refresh token dura más (días o semanas) y sirve para obtener
# un nuevo access token sin volver a pedir la contraseña.
# Flujo completo:
# 1. Login → access token (expira en 1h) + refresh token (expira en 30 días)
# 2. El cliente usa el access token en cada petición
# 3. Cuando el access token expira (401), usar el refresh token para obtener uno nuevo
# 4. Si el refresh token también expira, el usuario debe hacer login de nuevo
@app.post("/auth/login")
def login(email: str, contraseña: str):
   # ... verificar credenciales ...
   access_token  = crear_token({"sub": usuario_id}, expira_en_minutos=60)
   refresh_token = crear_token({"sub": usuario_id, "tipo": "refresh"}, expira_en_minutos=43200)  # 30 días
   # El refresh token se guarda en la BD para poder invalidarlo (logout)
   db.guardar_refresh_token(usuario_id, refresh_token)
   return {
       "access_token":  access_token,
       "refresh_token": refresh_token,
       "expira_en":     3600
   }
@app.post("/auth/refresh")
def refrescar_token(refresh_token: str):
   try:
       payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=["HS256"])
       # Verificar que el refresh token sigue siendo válido en la BD
       # (permite invalidar todos los tokens en un logout o cambio de contraseña)
       if not db.refresh_token_valido(payload["sub"], refresh_token):
           raise HTTPException(status_code=401, detail="Refresh token inválido")
       nuevo_access_token = crear_token({"sub": payload["sub"]}, expira_en_minutos=60)
       return {"access_token": nuevo_access_token, "expira_en": 3600}
   except jwt.ExpiredSignatureError:
       raise HTTPException(status_code=401, detail="Sesión expirada. Inicia sesión de nuevo")
@app.post("/auth/logout")
def logout(usuario = Depends(token_requerido)):
   # Invalidar el refresh token en la BD
   db.invalidar_refresh_tokens(usuario["sub"])
   return {"mensaje": "Sesión cerrada"}

Resumen

  • La autenticación verifica la identidad: ¿quién eres? La autorización verifica los permisos: ¿qué puedes hacer? Siempre van en ese orden.
  • Las contraseñas nunca se guardan en texto plano ni con MD5/SHA1. Usa bcrypt, Argon2 o scrypt con un coste de trabajo suficiente.
  • Los JWT son tokens firmados que contienen datos del usuario. La firma garantiza que no fueron modificados, pero el payload es solo base64, no cifrado: no incluyas datos sensibles.
  • RBAC asigna permisos a roles y roles a usuarios. Es el modelo más habitual y suficiente para la mayoría de aplicaciones.
  • La autorización tiene dos niveles: el rol determina qué tipo de operaciones puede hacer el usuario, y la propiedad del recurso determina sobre qué recursos específicos puede actuar.
  • Los errores de autenticación deben ser genéricos (no revelar si el email existe). Los errores de autorización devuelven 403 o 404 (si quieres ocultar la existencia del recurso).
  • Nunca confíes en datos del cliente para la autorización: el rol siempre debe venir del token verificado en el servidor, nunca de un parámetro que el usuario puede manipular.
  • El IDOR (Insecure Direct Object Reference) es el error más habitual: verificar autenticación pero olvidarse de verificar que el usuario tiene acceso al recurso concreto que está solicitando.

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a