Implementar Endpoints con Autenticación y Autorización

Lectura
25 min~7 min lectura

Concepto clave

En el desarrollo de APIs profesionales, la autenticación y autorización son dos pilares fundamentales de seguridad que trabajan en conjunto pero cumplen funciones distintas. Imagina que estás en un edificio corporativo: la autenticación es el guardia que verifica tu identidad con una credencial (como un token JWT), mientras que la autorización es el sistema que determina a qué pisos u oficinas puedes acceder según tu rol (como administrador o usuario regular).

En FastAPI, implementamos esto mediante dependencias que interceptan las solicitudes antes de llegar a los endpoints. La autenticación valida que el usuario sea quien dice ser, típicamente mediante tokens en el encabezado Authorization. La autorización luego verifica si ese usuario tiene permisos para realizar la acción solicitada, como crear, leer, actualizar o eliminar contenido. Este enfoque modular permite reutilizar lógica y mantener el código limpio y escalable.

Cómo funciona en la práctica

Vamos a construir un sistema paso a paso para nuestra API de gestión de contenido. Primero, definimos un modelo de usuario con roles (por ejemplo, 'admin', 'editor', 'viewer'). Luego, creamos una dependencia de autenticación que extrae y valida tokens JWT desde el encabezado HTTP. Si la autenticación falla, retornamos un error 401. Una vez autenticado, pasamos a la autorización: verificamos el rol del usuario contra los permisos requeridos para el endpoint específico.

Por ejemplo, para un endpoint que crea nuevo contenido, podríamos requerir el rol 'editor' o 'admin'. Si el usuario tiene rol 'viewer', devolvemos un error 403. FastAPI maneja esto elegantemente con decoradores como @app.post y parámetros de dependencia. La clave está en estructurar el código para que la lógica de seguridad sea centralizada y fácil de mantener, evitando duplicación y errores humanos.

Código en acción

Aquí tienes un ejemplo funcional de una dependencia de autenticación y autorización en FastAPI. Este código es copiable y ejecutable en un proyecto real.

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from typing import Optional
import jwt
from datetime import datetime, timedelta

app = FastAPI()
security = HTTPBearer()

# Configuración
SECRET_KEY = "tu_clave_secreta_super_segura"
ALGORITHM = "HS256"

# Modelos
class User(BaseModel):
    username: str
    role: str  # ej: "admin", "editor", "viewer"

class ContentItem(BaseModel):
    title: str
    body: str

# Simulación de base de datos de usuarios
fake_users_db = {
    "juan": User(username="juan", role="admin"),
    "maria": User(username="maria", role="editor"),
    "carlos": User(username="carlos", role="viewer")
}

# Función para crear token JWT
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=30)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

# Dependencia de autenticación
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
    token = credentials.credentials
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise HTTPException(status_code=401, detail="Token inválido")
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token expirado")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Token no válido")
    
    user = fake_users_db.get(username)
    if user is None:
        raise HTTPException(status_code=401, detail="Usuario no encontrado")
    return user

# Dependencia de autorización para roles específicos
def require_role(required_role: str):
    def role_dependency(current_user: User = Depends(get_current_user)):
        if current_user.role != required_role:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"Se requiere rol {required_role}"
            )
        return current_user
    return role_dependency

# Endpoint con autenticación y autorización
@app.post("/content/", response_model=ContentItem)
def create_content(
    item: ContentItem,
    current_user: User = Depends(require_role("editor"))
):
    # Solo usuarios con rol 'editor' o 'admin' (ya que admin también pasa el check de editor en este ejemplo)
    # Lógica para guardar en base de datos iría aquí
    return item

# Endpoint público sin autenticación
@app.get("/public/")
def public_content():
    return {"message": "Contenido público accesible para todos"}

# Endpoint solo para admin
@app.delete("/content/{content_id}")
def delete_content(
    content_id: int,
    current_user: User = Depends(require_role("admin"))
):
    return {"message": f"Contenido {content_id} eliminado por {current_user.username}"}

Ahora, veamos una mejora: refactorizar para permitir múltiples roles. Antes, require_role solo aceptaba un rol; después, podemos modificarlo para aceptar una lista.

# Versión mejorada: autorización para múltiples roles
def require_roles(allowed_roles: list):
    def roles_dependency(current_user: User = Depends(get_current_user)):
        if current_user.role not in allowed_roles:
            raise HTTPException(
                status_code=403,
                detail=f"Se requiere uno de estos roles: {allowed_roles}"
            )
        return current_user
    return roles_dependency

# Uso en endpoint
@app.put("/content/{content_id}")
def update_content(
    content_id: int,
    item: ContentItem,
    current_user: User = Depends(require_roles(["admin", "editor"]))
):
    return {"message": f"Contenido {content_id} actualizado"}

Errores comunes

  • No validar la expiración del token JWT: Si ignoras la fecha de expiración, los tokens podrían usarse indefinidamente, comprometiendo la seguridad. Siempre incluye y verifica el campo exp en el payload.
  • Almacenar secretos en el código fuente: Colocar claves como SECRET_KEY directamente en el archivo .py es riesgoso. Usa variables de entorno o un gestor de secretos para mantenerlas seguras.
  • Falta de manejo centralizado de errores: Si cada endpoint maneja errores de autenticación por separado, el código se vuelve repetitivo y propenso a inconsistencias. Centraliza la lógica en dependencias como se muestra arriba.
  • No limitar intentos de autenticación: Permitir intentos ilimitados de login puede facilitar ataques de fuerza bruta. Implementa throttling o bloqueos temporales después de varios fallos.
  • Confundir autenticación con autorización: Tratar la autenticación (quién eres) como suficiente para la autorización (qué puedes hacer) lleva a vulnerabilidades. Siempre verifica ambos pasos por separado.

Checklist de dominio

  1. ¿Puedes crear un token JWT válido con datos de usuario y expiración?
  2. ¿Implementas una dependencia que decodifica y valida tokens JWT en cada solicitud protegida?
  3. ¿Diferencias claramente entre endpoints públicos y privados en tu API?
  4. ¿Utilizas roles o permisos para controlar acceso a operaciones específicas (CRUD)?
  5. ¿Manejas errores de autenticación (401) y autorización (403) con mensajes claros y sin exponer detalles internos?
  6. ¿Has probado tu API con herramientas como Postman o curl, simulando usuarios con diferentes roles?
  7. ¿Documentas los requisitos de autenticación y autorización en la documentación automática de FastAPI (Swagger)?

Extender la API con Endpoints Seguros para Gestión de Usuarios

En este ejercicio, aplicarás los conceptos de autenticación y autorización para construir endpoints adicionales en tu API de gestión de contenido. Sigue estos pasos:

  1. Configura el entorno: Si no tienes un proyecto FastAPI en marcha, crea uno nuevo con el código de ejemplo de la lección como base. Asegúrate de tener instaladas las dependencias: fastapi, uvicorn, python-jose[cryptography] para JWT, y pydantic.
  2. Agrega un endpoint de login: Crea un endpoint POST /login que acepte username y password (puedes simular la validación con fake_users_db). Si las credenciales son válidas, genera y retorna un token JWT que incluya el username y rol del usuario. Usa la función create_access_token del ejemplo.
  3. Implementa un endpoint protegido para listar usuarios: Crea un endpoint GET /users/ que retorne la lista de usuarios desde fake_users_db. Este endpoint debe requerir autenticación (usando get_current_user) y solo permitir acceso a usuarios con rol admin. Usa la dependencia require_role o require_roles.
  4. Añade un endpoint para actualizar roles: Desarrolla un endpoint PUT /users/{username}/role que permita cambiar el rol de un usuario (por ejemplo, de 'viewer' a 'editor'). Requiere autenticación y autorización para rol admin. Valida que el nuevo rol sea uno de los permitidos (admin, editor, viewer).
  5. Prueba y documenta: Ejecuta la API con uvicorn main:app --reload y prueba todos los endpoints usando Postman o curl. Verifica que los tokens funcionen y los permisos se apliquen correctamente. Revisa la documentación automática en http://localhost:8000/docs para asegurarte de que los requisitos de seguridad estén claros.
Pistas
  • Usa el código de la lección como punto de partida y modifica solo lo necesario para los nuevos endpoints.
  • Para el endpoint de login, considera devolver el token en un formato JSON estándar, como {"access_token": "tu_token", "token_type": "bearer"}.
  • En la validación de roles, asegúrate de manejar casos edge, como intentar cambiar el rol de un usuario que no existe.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.