Práctica: Construir un Sistema de Login con Roles y Protección

Lectura
25 min~5 min lectura

Concepto clave

En el desarrollo de APIs profesionales, la autenticación y autorización son dos pilares fundamentales que trabajan en conjunto para proteger los recursos. La autenticación responde a la pregunta "¿Quién eres?" mediante la verificación de credenciales, como un usuario y contraseña. Una vez autenticado, la autorización determina "¿Qué puedes hacer?" basándose en roles o permisos asignados.

Imagina un edificio de oficinas: la autenticación es como mostrar tu identificación en recepción para entrar, mientras que la autorización son las llaves específicas que te permiten acceder solo a ciertas salas según tu puesto. En FastAPI, implementamos esto combinando OAuth2 con contraseña para autenticación y dependencias personalizadas para autorización, creando un sistema robusto y escalable.

Cómo funciona en la práctica

El flujo comienza cuando un usuario envía sus credenciales a un endpoint de login. FastAPI valida estas credenciales contra una base de datos y, si son correctas, genera un token JWT (JSON Web Token) que contiene información del usuario y sus roles. Este token se envía al cliente y debe incluirse en las cabeceras de las solicitudes posteriores.

Para proteger endpoints específicos, creamos dependencias que: 1) Verifican la validez del token, 2) Extraen la información del usuario, y 3) Comprueban si tiene los roles necesarios. Por ejemplo, un endpoint para eliminar usuarios podría requerir el rol "admin", mientras que uno para ver perfiles solo requeriría estar autenticado. Este enfoque permite un control granular sobre quién puede acceder a cada recurso.

Código en acción

Primero, configuramos la autenticación con OAuth2 y JWT:

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from pydantic import BaseModel

app = FastAPI()

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

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

class Token(BaseModel):
    access_token: str
    token_type: str

class User(BaseModel):
    username: str
    email: str
    full_name: str
    disabled: bool
    roles: list

# Función para verificar contraseña
def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

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

Luego, implementamos la autorización con roles:

from fastapi import Depends
from typing import List

# Dependencia para obtener usuario actual
def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Credenciales inválidas",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        roles: list = payload.get("roles", [])
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    return {"username": username, "roles": roles}

# Dependencia para verificar roles
def require_roles(required_roles: List[str]):
    def role_checker(current_user: dict = Depends(get_current_user)):
        user_roles = current_user.get("roles", [])
        if not any(role in user_roles for role in required_roles):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Permisos insuficientes"
            )
        return current_user
    return role_checker

# Endpoint protegido con roles
@app.get("/admin/dashboard")
async def admin_dashboard(current_user: dict = Depends(require_roles(["admin"]))):
    return {"message": "Bienvenido al panel de administración", "user": current_user}

@app.get("/user/profile")
async def user_profile(current_user: dict = Depends(get_current_user)):
    return {"message": "Perfil de usuario", "user": current_user}

Errores comunes

  • Almacenar contraseñas en texto plano: Nunca guardes contraseñas sin encriptar. Usa bibliotecas como passlib con bcrypt para hashearlas de forma segura antes de almacenarlas en la base de datos.
  • Exponer información sensible en tokens JWT: Los tokens pueden ser decodificados (aunque no modificados si están firmados). No incluyas datos sensibles como contraseñas o información personal crítica en el payload del token.
  • No validar roles en el backend: Aunque el frontend pueda ocultar elementos basados en roles, siempre valida los permisos en el backend. Un usuario malintencionado podría enviar solicitudes directamente a la API.
  • Usar tiempos de expiración muy largos: Los tokens JWT deben tener una vida útil corta (minutos u horas) para minimizar el riesgo si son comprometidos. Implementa un sistema de refresh tokens para sesiones prolongadas.
  • Falta de rate limiting en endpoints de login: Los endpoints de autenticación son objetivos comunes para ataques de fuerza bruta. Implementa límites de intentos para prevenir estos ataques.

Checklist de dominio

  1. Puedo implementar autenticación OAuth2 con contraseña y tokens JWT en FastAPI
  2. Sé crear y verificar hashes de contraseñas usando bcrypt
  3. Puedo proteger endpoints con dependencias que verifican tokens y roles
  4. Entiendo la diferencia entre autenticación (verificar identidad) y autorización (verificar permisos)
  5. Sé implementar un sistema de roles jerárquicos (ej: admin > editor > user)
  6. Puedo manejar correctamente errores de autenticación (401) y autorización (403)
  7. Sé configurar tiempos de expiración adecuados para tokens de acceso y refresh

Implementa un sistema de gestión de usuarios con roles múltiples

En este ejercicio práctico, construirás un sistema completo de gestión de usuarios con autenticación y autorización basada en roles. Sigue estos pasos:

  1. Crea un nuevo proyecto FastAPI con las siguientes dependencias: fastapi, uvicorn, python-jose[cryptography], passlib[bcrypt], python-multipart
  2. Implementa un modelo de usuario en una base de datos SQL (puedes usar SQLAlchemy o databases) con los campos: id, username, email, hashed_password, is_active, roles (lista de strings)
  3. Crea los siguientes endpoints:
    • POST /register - Permite registrar nuevos usuarios (solo rol "user" por defecto)
    • POST /login - Devuelve un token JWT con los roles del usuario
    • GET /users/me - Devuelve el perfil del usuario autenticado
    • PUT /users/{user_id} - Permite a usuarios con rol "admin" actualizar cualquier usuario, o a usuarios normales actualizar solo su propio perfil
    • DELETE /users/{user_id} - Solo accesible por usuarios con rol "admin"
  4. Implementa un sistema de roles que incluya: "admin" (acceso completo), "editor" (puede crear/editar contenido), "user" (acceso básico)
  5. Añade rate limiting al endpoint /login (máximo 5 intentos por minuto por IP)
  6. Escribe tests para verificar:
    • Un usuario normal no puede acceder a endpoints de admin
    • Un usuario puede actualizar solo su propio perfil
    • El token JWT expira correctamente
Pistas
  • Usa el decorador @app.middleware("http") para implementar el rate limiting basado en IP
  • Considera usar una tabla separada para la relación muchos-a-muchos entre usuarios y roles si necesitas escalabilidad
  • Para el endpoint PUT /users/{user_id}, crea una dependencia que verifique si el usuario es admin O si está actualizando su propio perfil

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.