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_usery una dependenciaSecurityconsciente de scopes permite rutas protegidas de forma segura. - En sistemas reales de producción, pensar en el diseño global—refresh 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.
- Una clase de dependencia que extrae la cadena del token desde el encabezado
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,
localStoragees 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
- Implementar contraseñas con hash y una gestión simple de usuarios.
- Construir login JWT mínimo + rutas protegidas con
/auth/tokenyget_current_user. - En desarrollo, acortar la expiración del access token y experimentar errores de “token expirado”.
- 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
- Identificar endpoints donde realmente quieres comprobación por scopes.
- Organizar una lista de roles y scopes y crear una tabla de mapeo (empezar pequeño).
- Añadir comprobaciones de scopes usando dependencias
Security, comenzando por endpoints críticos. - 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
- Consolidar auth/authz JWT para humanos (roles, scopes, refresh tokens).
- Organizar patrones para claves API y verificación de firmas en llamadas servicio-a-servicio (incluyendo webhooks).
- Documentar la imagen completa de seguridad: HTTPS, CORS, rate limiting, logs de auditoría, etc., y alinear al equipo.
- 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_userySecurityconsciente 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.

