Icono del sitio IT&ライフハックブログ|学びと実践のためのアイデア集

Guía práctica de seguridad en FastAPI: diseño de APIs modernas protegidas con autenticación JWT, scopes OAuth2 y claves API

green snake

Photo by Pixabay on Pexels.com

Guía práctica de seguridad en FastAPI: diseño de APIs modernas protegidas con autenticación JWT, scopes OAuth2 y claves API


Resumen (pirámide invertida)

  • FastAPI incluye primitivas de seguridad (Depends, Security, OAuth2) listas para usar, lo que facilita implementar autenticación/autorización práctica con JWT y claves API.
  • La autenticación verifica quién eres; la autorización decide qué se te permite hacer. El diseño de JWT y el diseño de scopes (permisos) son las piezas clave.
  • En una configuración simple, combinar hashing de contraseñas, access tokens de corta duración (JWT), una dependencia get_current_user y una dependencia Security consciente de scopes permite rutas protegidas de forma segura.
  • En sistemas reales de producción, pensar en el diseño globalrefresh tokens, organización de roles/scopes, claves API y webhooks firmados, HTTPS y CORS—lleva a una API robusta y más fácil de operar a largo plazo.
  • Este artículo organiza tanto “lo básico que debes clavar primero” como “puntos de producción un paso más profundos” con ejemplos de código concretos, e incluso explica una hoja de ruta de adopción por fases.

A quién le beneficia leer esto (personas concretas)

Desarrolladores individuales / aprendices

  • Quieres construir un pequeño servicio web con inicio de sesión, o una SPA + backend en FastAPI.
  • Seguiste tutoriales, pero JWT y OAuth2 aún se sienten confusos y te generan inseguridad.
  • Quieres aprender primero una “configuración mínima que se vea segura” y luego evolucionarla gradualmente.

Para ti, esta guía te permite ejecutar un login simple con JWT y una ruta protegida, experimentando el conjunto esencial: hashing de contraseñas, expiración del access token y más.

Ingenieros backend en equipos pequeños

  • Tu UI de administración, API de usuarios finales y herramientas internas se están volviendo un caos, y te cuesta organizar “quién puede hacer qué”.
  • Quieres modelar roles/permisos y conectarlos limpiamente con las dependencias de seguridad de FastAPI.
  • Necesitas una estructura que sobreviva cambios frecuentes de especificación sin romper la lógica de autorización.

Para ti, serán útiles los patrones con scopes OAuth2, tablas de mapeo rol→scope y la protección de endpoints con dependencias Security.

Equipos SaaS / startups

  • Estás añadiendo más “clientes no humanos” como APIs de socios, webhooks y llamadas servicio-a-servicio.
  • Quieres organizar directrices para claves API, webhooks firmados y autenticación de servicios zero-trust.
  • Quieres una base que no te encierre cuando más adelante añadas MFA o integres con un IDaaS.

Para ti, la separación de responsabilidades entre “JWT de usuario”, “claves API de servicio” y “firmas de webhook”, además de la visión completa de seguridad, debería ser útil.


Evaluación de accesibilidad (legibilidad y consideración)

  • Estructura: comienza con resumen y lectores objetivo, y sigue un flujo de pirámide invertida: “conceptos básicos → implementación JWT → autorización por scopes → claves API → precauciones de producción → pruebas → hoja de ruta”.
  • Redacción: los términos técnicos se explican brevemente al primer uso y luego se reutilizan de forma consistente para evitar confusiones. El tono es amable, pero las explicaciones siguen siendo concretas.
  • Ejemplos de código: se mantienen legibles en bloques monoespaciados y se dividen para evitar saturación. Los comentarios se limitan a lo necesario para reducir la verbosidad.
  • Lector previsto: asume que ya tocaste el tutorial básico de FastAPI al menos una vez, pero cada sección está escrita para que puedas “medio entenderla” incluso si solo lees ese capítulo.

En conjunto, apunta aproximadamente a un nivel de accesibilidad tipo WCAG AA como artículo técnico.


1. Fundamentos de autenticación y autorización: ¿qué estamos protegiendo?

Primero, organicemos rápidamente qué significan “autenticación”, “autorización” y “tokens”.

1.1 Autenticación

  • Un mecanismo para confirmar “¿Quién eres?”
  • Incluye cualquier método para probar identidad: usuario + contraseña, login social, códigos de un solo uso, etc.
  • En FastAPI, un flujo común es: validar usuario/contraseña desde un formulario y luego emitir un token si es correcto.

1.2 Autorización

  • Un mecanismo para decidir “¿Qué se te permite hacer?”
  • Distingue operaciones que solo admins pueden hacer, lo que usuarios normales pueden hacer, lo que invitados no pueden hacer, etc.
  • En FastAPI, un patrón común es: almacenar roles o scopes en el payload del JWT y comprobar scopes mediante dependencias Security.

1.3 Tokens

  • Un token es como un certificado temporal que un cliente usa para mostrarle al servidor “ya me autenticé”.
  • En APIs web modernas, se usa mucho JWT (JSON Web Token)—un token autocontenido.
    • Es una cadena compuesta por cabecera, payload y firma, firmada con una clave secreta del servidor.
    • El servidor puede verificar la firma cada vez y decidir si el contenido del token es confiable sin consultar la BD en cada petición.

Uniendo todo, construyes un flujo como: “login → emitir JWT → cada endpoint verifica JWT y comprueba scopes.”


2. Mecanismos de seguridad que ofrece FastAPI

FastAPI incluye varias funcionalidades relacionadas con seguridad. Veamos los nombres primero.

  • OAuth2PasswordBearer
    • Una clase de dependencia que extrae la cadena del token desde el encabezado Authorization: Bearer <token>.
    • Aun debes implementar la verificación (validación de firma, comprobación de expiración, etc.), pero puedes delegar la extracción del token.
  • Security
    • Un mecanismo de dependencias para declarar cosas como “este endpoint requiere estos scopes”.
    • Con un esquema OAuth2, puedes especificar scopes requeridos como scopes=["items:read"].
  • Módulo fastapi.security
    • Una colección de clases auxiliares para autenticación por contraseña y por clave API (login por formulario, HTTP Basic, claves API en header/query/cookie, etc.).

Un “patrón estándar” es: usar estos helpers y completar tú mismo la generación/verificación de JWT.


3. Implementar una configuración mínima de autenticación JWT

Ahora entremos en código real. Para que sea fácil de visualizar, construiremos un “API mínima de login + ruta protegida”.

3.1 Dependencias de ejemplo

  • Una librería JWT (p. ej., PyJWT)
  • Hashing de contraseñas (p. ej., passlib[bcrypt])

Instalación de ejemplo (ilustrativa):

pip install PyJWT passlib[bcrypt]

3.2 Modelo de usuario y hashing de contraseñas

Empecemos con una gestión de usuarios muy simplificada.

# app/core/security.py
from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(plain_password: str) -> str:
    return pwd_context.hash(plain_password)

def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)
# app/models/user.py (pseudo BD para el ejemplo)
from dataclasses import dataclass

@dataclass
class User:
    id: int
    username: str
    hashed_password: str
    is_active: bool = True
    roles: list[str] = None

fake_users_db: dict[str, User] = {}

Crea un usuario inicial.

# app/fixtures/users_init.py (se asume que corre una vez al inicio)
from app.models.user import User, fake_users_db
from app.core.security import hash_password

def init_users():
    fake_users_db["alice"] = User(
        id=1,
        username="alice",
        hashed_password=hash_password("password123"),
        roles=["user"],
    )

En producción usarías una BD real, pero esto se simplifica a propósito para captar el flujo general.

3.3 Creación y validación del token JWT

Prepara funciones utilitarias para crear y validar tokens.

# app/core/jwt.py
from datetime import datetime, timedelta, timezone
from typing import Any, Optional

import jwt  # PyJWT

from app.core.settings import settings  # settings con SECRET_KEY, etc.

ALGORITHM = "HS256"

def create_access_token(
    data: dict[str, Any],
    expires_delta: Optional[timedelta] = None,
) -> str:
    to_encode = data.copy()
    now = datetime.now(timezone.utc)
    if expires_delta:
        expire = now + expires_delta
    else:
        expire = now + timedelta(minutes=30)
    to_encode.update({"exp": expire, "iat": now})
    encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=ALGORITHM)
    return encoded_jwt

def decode_access_token(token: str) -> dict[str, Any]:
    # Si falla la validación, se lanzará una excepción derivada de jwt.PyJWTError
    payload = jwt.decode(token, settings.secret_key, algorithms=[ALGORITHM])
    return payload

Un patrón común es guardar el nombre de usuario o el ID de usuario en el payload como sub (subject).

3.4 Endpoint /token (login)

En un estilo de autenticación OAuth2 con contraseña, acepta usuario y contraseña como datos de formulario.

# app/api/auth.py
from datetime import timedelta

from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm

from app.core.security import verify_password
from app.core.jwt import create_access_token
from app.models.user import fake_users_db, User

router = APIRouter(prefix="/auth", tags=["auth"])

ACCESS_TOKEN_EXPIRE_MINUTES = 30

def authenticate_user(username: str, password: str) -> User | None:
    user = fake_users_db.get(username)
    if not user:
        return None
    if not verify_password(password, user.hashed_password):
        return None
    if not user.is_active:
        return None
    return user

@router.post("/token")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="El nombre de usuario o la contraseña son incorrectos",
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username, "roles": user.roles},
        expires_delta=access_token_expires,
    )
    return {
        "access_token": access_token,
        "token_type": "bearer",
    }

El cliente envía username y password como formulario a /auth/token y luego incluye el access_token devuelto en Authorization: Bearer <token> para llamadas posteriores.

3.5 Dependencia get_current_user y una ruta protegida

Define una dependencia que valida el token y extrae el usuario actual.

# app/deps/auth.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

from app.core.jwt import decode_access_token
from app.models.user import fake_users_db, User

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")

def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    try:
        payload = decode_access_token(token)
    except Exception:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="El token no es válido o ha expirado",
        )
    username: str | None = payload.get("sub")
    if username is None:
        raise HTTPException(status_code=401, detail="No hay información de usuario en el token")
    user = fake_users_db.get(username)
    if user is None or not user.is_active:
        raise HTTPException(status_code=401, detail="El usuario no existe o está inactivo")
    return user

Úsala para crear un endpoint protegido.

# app/api/me.py
from fastapi import APIRouter, Depends
from app.models.user import User
from app.deps.auth import get_current_user

router = APIRouter(prefix="/me", tags=["me"])

@router.get("")
def read_me(current_user: User = Depends(get_current_user)):
    # Solo usuarios con sesión iniciada pueden acceder
    return {
        "id": current_user.id,
        "username": current_user.username,
        "roles": current_user.roles,
    }

En este punto ya tienes el flujo mínimo: “iniciar sesión → recibir access token → usar el token para obtener tus datos”.


4. Autorización: permisos finos con roles y scopes

Lo siguiente es la autorización: distinguir “quién puede hacer qué”.
Antes guardamos roles en el payload, pero para control más fino, los scopes (permisos) suelen ser más claros.

4.1 Relación entre roles y scopes

  • Rol: etiquetas legibles para humanos como admin, staff, user
  • Scope: permisos a nivel API como articles:read, articles:write, users:manage

Un enfoque típico es:

  • Mantener una tabla de mapeo: “este rol otorga estos scopes
  • Poner una lista de scopes dentro del payload del token
  • Los endpoints declaran “se requiere este scope

4.2 Dependencia de seguridad con scopes OAuth2

En FastAPI puedes pasar una lista de scopes a OAuth2PasswordBearer.

# app/deps/auth_scoped.py
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
from fastapi import Depends, HTTPException, status

from app.core.jwt import decode_access_token
from app.models.user import fake_users_db, User

oauth2_scheme_scoped = OAuth2PasswordBearer(
    tokenUrl="/auth/token",
    scopes={
        "articles:read": "Leer artículos",
        "articles:write": "Crear/actualizar/eliminar artículos",
        "users:manage": "Administrar usuarios",
    },
)

def get_current_user_scoped(
    security_scopes: SecurityScopes,
    token: str = Depends(oauth2_scheme_scoped),
) -> User:
    try:
        payload = decode_access_token(token)
    except Exception:
        raise HTTPException(status_code=401, detail="Token inválido")

    username: str | None = payload.get("sub")
    token_scopes: list[str] = payload.get("scopes", [])
    if username is None:
        raise HTTPException(status_code=401, detail="No hay información de usuario")
    user = fake_users_db.get(username)
    if not user:
        raise HTTPException(status_code=401, detail="El usuario no existe")

    # Verifica que todos los *scopes* requeridos estén incluidos en los *scopes* del token
    for scope in security_scopes.scopes:
        if scope not in token_scopes:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"Esta operación requiere el scope '{scope}'",
            )
    return user

4.3 Endpoints que requieren scopes

Del lado del router, usa Security con scopes.

# app/api/articles.py
from fastapi import APIRouter, Security
from app.deps.auth_scoped import get_current_user_scoped
from app.models.user import User

router = APIRouter(prefix="/articles", tags=["articles"])

@router.get("")
def list_articles(
    current_user: User = Security(get_current_user_scoped, scopes=["articles:read"])
):
    # Listar artículos: basta con scope de lectura
    return [{"id": 1, "title": "sample"}]

@router.post("")
def create_article(
    current_user: User = Security(get_current_user_scoped, scopes=["articles:write"])
):
    # Crear artículo: requiere scope de escritura
    return {"status": "created"}

Como puedes declarar “este scope es requerido” por endpoint, es fácil alinearlo con tus specs/docs.

4.4 Asignar scopes a partir de roles

Si calculas scopes desde una tabla rol→scope al emitir el token y los guardas en el payload, todo queda más limpio.

# app/core/roles.py
ROLE_SCOPES = {
    "admin": ["articles:read", "articles:write", "users:manage"],
    "editor": ["articles:read", "articles:write"],
    "user": ["articles:read"],
}

def scopes_for_roles(roles: list[str]) -> list[str]:
    scopes: set[str] = set()
    for role in roles:
        scopes.update(ROLE_SCOPES.get(role, []))
    return sorted(scopes)

Úsalo al emitir el token.

# app/api/auth.py (cambio parcial)
from app.core.roles import scopes_for_roles

@router.post("/token")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
    # ... la autenticación es la misma ...
    scopes = scopes_for_roles(user.roles or [])
    access_token = create_access_token(
        data={"sub": user.username, "scopes": scopes},
        expires_delta=access_token_expires,
    )
    return {"access_token": access_token, "token_type": "bearer"}

Así, si más adelante revisas roles/permisos, solo necesitas cambios mínimos alrededor de la tabla de mapeo.


5. Comunicación servicio-a-servicio y autenticación con clave API: proteger clientes no humanos

No solo los humanos llamarán a tu FastAPI—otros servicios y batch jobs también. En esos casos, las claves API pueden ser más simples que JWT.

5.1 Concepto de autenticación con clave API

  • El servidor emite un par como “ID de cliente” y “secreto (clave API)”.
  • El cliente la envía en un encabezado como x-api-key: <secret>.
  • FastAPI la valida y decide si el cliente está autorizado.

FastAPI (fastapi.security) también ofrece helpers para claves API.

5.2 Ejemplo simple de dependencia para clave API

# app/deps/api_key.py
from fastapi import Depends, HTTPException, Security, status
from fastapi.security.api_key import APIKeyHeader

# En producción, asume que esto viene de BD o configuración
VALID_API_KEYS = {
    "service-a": "secret-key-for-service-a",
}

api_key_header = APIKeyHeader(name="x-api-key", auto_error=False)

def get_current_service(api_key: str | None = Security(api_key_header)) -> str:
    if api_key is None:
        raise HTTPException(status_code=401, detail="No se proporcionó una clave API")
    for service_name, key in VALID_API_KEYS.items():
        if api_key == key:
            return service_name
    raise HTTPException(status_code=403, detail="Clave API inválida")
# app/api/internal.py
from fastapi import APIRouter, Depends
from app.deps.api_key import get_current_service

router = APIRouter(prefix="/internal", tags=["internal"])

@router.post("/sync")
def sync_something(service: str = Depends(get_current_service)):
    # service contiene un identificador como "service-a"
    return {"from": service, "status": "ok"}

Para webhooks y comunicación servicio-a-servicio, es común combinar claves API con restricciones por IP y firmas (HMAC), etc.


6. Puntos prácticos de seguridad a vigilar en producción

Aquí van puntos importantes desde una perspectiva operativa.

6.1 HTTPS es obligatorio

  • Los datos de login y tokens deben intercambiarse por HTTPS.
  • Evita operar por HTTP plano fuera del desarrollo local (está bien si TLS termina en un proxy/balanceador).

6.2 Mantén los access tokens de corta duración

  • Vidas cortas (minutos a decenas de minutos) reducen el impacto si un token se filtra.
  • Para sesiones largas, combina con refresh tokens (almacenados en BD o almacenamiento seguro).

6.3 Dónde almacenar tokens

  • En navegadores, localStorage es más fácil de robar vía XSS, así que considerar cookies HttpOnly suele valer la pena.
  • En apps móviles o clientes del lado servidor, usa almacenamiento seguro del SO/entorno (p. ej., llavero).

6.4 Restringe CORS y orígenes

  • Si la API se llama directamente desde navegadores, limita los orígenes permitidos (Access-Control-Allow-Origin) al mínimo necesario.
  • Usa el middleware de CORS de FastAPI con ajustes “amplios en dev, estrictos en prod”.

6.5 Logs y auditoría

  • Registra fallos de auth/authz (fallos de login, permisos insuficientes) como logs de auditoría para detectar accesos sospechosos.
  • Pero no registres contraseñas en bruto ni el contenido completo del token—mantén los logs mínimos (p. ej., usuario y resultado).

7. Proteger la seguridad con pruebas: dependency overrides y validación de scopes

La seguridad es un área donde los fallos son “difíciles de notar”, así que conviene usar pruebas automatizadas para proteger una línea base mínima.

7.1 Probar el API de login

  • ¿Usuario/contraseña correctos devuelven un token?
  • ¿Una contraseña incorrecta devuelve 401?
  • ¿Cambió inesperadamente el formato de respuesta (nombres de campos)?
# tests/test_auth_api.py
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_login_success():
    res = client.post("/auth/token", data={"username": "alice", "password": "password123"})
    assert res.status_code == 200
    body = res.json()
    assert "access_token" in body
    assert body["token_type"] == "bearer"

def test_login_failure():
    res = client.post("/auth/token", data={"username": "alice", "password": "wrong"})
    assert res.status_code == 401

7.2 Probar rutas protegidas

  • Sin token, ¿devuelve 401?
  • Con scopes insuficientes, ¿devuelve 403?
  • Con scopes correctos, ¿devuelve 200?

Puedes generar tokens desde el código de prueba y ponerlos en headers para verificar la lógica de autorización.


8. Hoja de ruta de adopción por tipo de lector

Reorganicemos el contenido como “por dónde empezar”.

Pasos para desarrolladores individuales / aprendices

  1. Implementar contraseñas con hash y una gestión simple de usuarios.
  2. Construir login JWT mínimo + rutas protegidas con /auth/token y get_current_user.
  3. En desarrollo, acortar la expiración del access token y experimentar errores de “token expirado”.
  4. Ejecutar el flujo completo desde un frontend (p. ej., SPA) para entender el ciclo de vida del token.

Pasos para ingenieros en equipos pequeños

  1. Identificar endpoints donde realmente quieres comprobación por scopes.
  2. Organizar una lista de roles y scopes y crear una tabla de mapeo (empezar pequeño).
  3. Añadir comprobaciones de scopes usando dependencias Security, comenzando por endpoints críticos.
  4. Estructurar la app para que cambios de permisos se manejen vía capas de servicio y la tabla de scopes.

Pasos para equipos SaaS / startups

  1. Consolidar auth/authz JWT para humanos (roles, scopes, refresh tokens).
  2. Organizar patrones para claves API y verificación de firmas en llamadas servicio-a-servicio (incluyendo webhooks).
  3. Documentar la imagen completa de seguridad: HTTPS, CORS, rate limiting, logs de auditoría, etc., y alinear al equipo.
  4. Diseñar con separación de responsabilidades (plataforma de autenticación vs. app) para prepararse para integraciones futuras con IDaaS/MFA.

9. Cierre: hacer que la seguridad en FastAPI “no dé miedo”

  • La autenticación confirma “quién”, la autorización decide “qué”, y FastAPI trae bloques estándar (Depends, Security, OAuth2) que soportan ambos.
  • Incluso con una configuración simple de JWT, combinar hashing de contraseñas, access tokens de corta vida, una dependencia get_current_user y Security consciente de scopes da una base de seguridad muy práctica.
  • En producción, considerar “toda la defensa”: diseño de roles/scopes, claves/firmas para auth de servicios, HTTPS/CORS/logs de auditoría, etc., da una base que sobrevive cambios futuros de especificación y de funcionalidades.
  • No necesitas seguridad perfecta desde el día uno. Empieza con login JWT mínimo + rutas protegidas y luego amplía gradualmente a scopes, claves API y pruebas—paso a paso—para hacer crecer una “seguridad que no da miedo”.

Si esta guía te ayuda a dar un paso hacia publicar y operar tu app FastAPI con seguridad, me alegraría de verdad.
Ve a un ritmo sostenible y prueba las cosas una por una. Te acompaño en silencio para que tu servicio crezca de forma segura y constante.


Salir de la versión móvil