Quiz: Seguridad y Autenticación en FastAPI

Quiz
10 min~4 min lectura

Quiz Interactivo

Pon a prueba tus conocimientos

Concepto clave

La seguridad en APIs modernas se basa en tres pilares fundamentales: autenticación (verificar quién eres), autorización (qué puedes hacer) y protección de datos (cómo se maneja la información sensible). En FastAPI, esto se implementa mediante middlewares, dependencias y estándares como OAuth2 con JWT.

Imagina una empresa con acceso controlado: primero muestras tu credencial (autenticación), luego el sistema verifica a qué áreas tienes permiso (autorización), y finalmente asegura que no puedas ver información confidencial de otros departamentos (protección de datos). FastAPI proporciona herramientas nativas para construir este flujo de manera eficiente y escalable.

Cómo funciona en la práctica

El proceso típico en FastAPI sigue estos pasos:

  1. El cliente envía credenciales (usuario/contraseña) a un endpoint de login
  2. El servidor valida las credenciales contra una base de datos
  3. Si son válidas, genera un token JWT firmado
  4. El cliente incluye este token en el header Authorization de solicitudes posteriores
  5. FastAPI valida automáticamente el token usando dependencias
  6. Se verifica si el usuario tiene permisos para la acción solicitada

Este flujo permite stateless authentication, ideal para arquitecturas microservicios.

Código en acción

Implementación básica de autenticación JWT:

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

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

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

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

class TokenData(BaseModel):
    username: Optional[str] = None

# 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
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=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

# Dependencia para obtener usuario actual
async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except JWTError:
        raise credentials_exception
    return token_data

@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    # Aquí iría la validación real contra base de datos
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}

@app.get("/users/me")
async def read_users_me(current_user: TokenData = Depends(get_current_user)):
    return {"username": current_user.username}

Implementación mejorada con roles y permisos:

from enum import Enum
from fastapi import Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer

class Role(str, Enum):
    ADMIN = "admin"
    USER = "user"
    GUEST = "guest"

security = HTTPBearer()

def has_permission(required_role: Role):
    async def permission_dependency(
        credentials: HTTPAuthorizationCredentials = Security(security)
    ):
        token = credentials.credentials
        try:
            payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
            user_role = payload.get("role")
            
            # Jerarquía de permisos
            role_hierarchy = {Role.GUEST: 1, Role.USER: 2, Role.ADMIN: 3}
            
            if role_hierarchy.get(Role(user_role), 0) < role_hierarchy[required_role]:
                raise HTTPException(
                    status_code=status.HTTP_403_FORBIDDEN,
                    detail="Insufficient permissions"
                )
            return payload
        except JWTError:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid token"
            )
    return permission_dependency

@app.get("/admin/dashboard")
async def admin_dashboard(
    user_data = Depends(has_permission(Role.ADMIN))
):
    return {"message": "Welcome to admin dashboard", "user": user_data}

Errores comunes

  • No validar expiración de tokens: Los tokens JWT deben tener expiración configurada y validarse en cada request. Sin esto, un token robado es válido indefinidamente.
  • Almacenar secretos en código: Claves secretas como SECRET_KEY deben estar en variables de entorno, nunca hardcodeadas en el código fuente.
  • Falta de rate limiting: Endpoints de login deben tener límites de intentos para prevenir ataques de fuerza bruta.
  • No usar HTTPS en producción: Sin HTTPS, tokens viajan en texto plano, exponiéndolos a interceptación.
  • Permisos demasiado amplios: Asignar permisos de administrador por defecto o no implementar principio de menor privilegio.

Checklist de dominio

  1. Implementé autenticación JWT con expiración configurable
  2. Protegí endpoints con dependencias que validan tokens automáticamente
  3. Implementé sistema de roles y permisos con jerarquía
  4. Configuré variables de entorno para claves secretas
  5. Agregué rate limiting a endpoints críticos
  6. Validé que todos los tokens incluyan claim "exp"
  7. Documenté los flujos de autenticación en OpenAPI/Swagger

Implementa un sistema de autenticación con refresh tokens

En este ejercicio, mejorarás el sistema de autenticación básico para incluir refresh tokens, una práctica esencial en APIs profesionales.

  1. Crea un nuevo endpoint /refresh que acepte un refresh token válido y devuelva un nuevo access token
  2. Modifica el endpoint /token para que devuelva tanto access_token como refresh_token
  3. Implementa lógica para invalidar refresh tokens cuando sean comprometidos
  4. Asegúrate que los refresh tokens tengan mayor tiempo de expiración que los access tokens (ej: 7 días vs 30 minutos)
  5. Crea un endpoint /logout que permita invalidar ambos tokens
  6. Agrega validación para prevenir reuso de refresh tokens

Entrega: Código completo con los 3 endpoints funcionando y pruebas que demuestren el flujo completo.

Pistas
  • Considera almacenar refresh tokens en una base de datos para poder invalidarlos
  • Usa diferentes claves secretas para access y refresh tokens
  • Implementa blacklist para tokens invalidados

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.