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_jwtLuego, 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
- Puedo implementar autenticación OAuth2 con contraseña y tokens JWT en FastAPI
- Sé crear y verificar hashes de contraseñas usando bcrypt
- Puedo proteger endpoints con dependencias que verifican tokens y roles
- Entiendo la diferencia entre autenticación (verificar identidad) y autorización (verificar permisos)
- Sé implementar un sistema de roles jerárquicos (ej: admin > editor > user)
- Puedo manejar correctamente errores de autenticación (401) y autorización (403)
- 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:
- Crea un nuevo proyecto FastAPI con las siguientes dependencias: fastapi, uvicorn, python-jose[cryptography], passlib[bcrypt], python-multipart
- 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)
- 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"
- Implementa un sistema de roles que incluya: "admin" (acceso completo), "editor" (puede crear/editar contenido), "user" (acceso básico)
- Añade rate limiting al endpoint /login (máximo 5 intentos por minuto por IP)
- 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
- 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.