Autenticación y seguridad
¿Qué es?
Autenticación es el proceso de saber quién hace cada petición a tu API. Seguridad, aquí, es proteger ciertas rutas para que solo personas identificadas puedan usarlas. En este módulo aprendes a guardar contraseñas de forma segura (como un hash irreversible, nunca en texto plano) y a entregar tokens JWT: textos firmados que el cliente reenvía para probar quién es sin volver a mandar su contraseña.
¿Cómo funciona?
Cuando un usuario inicia sesión, compruebas su contraseña contra el hash guardado y, si coincide, le entregas un JWT firmado con una clave secreta. En cada petición posterior, el cliente envía ese token; una dependencia de FastAPI (Depends) lo decodifica, verifica la firma y obtiene quién es. Si el token falta o es inválido, el endpoint responde 401 y ni siquiera se ejecuta. Como el token está firmado, nadie puede falsificarlo sin la clave secreta.
¿Para qué sirve?
Sin autenticación, cualquiera podría modificar o borrar tus datos; con ella, decides quién puede hacer qué. Es un requisito básico para que una API se use en serio y no solo en pruebas. En el proyecto final, «API para tus herramientas», esto permite exigir que solo un docente autenticado registre o borre notas, dejando registro de quién hizo cada acción.
Una API que cualquiera puede modificar no sirve para producción. En este módulo aprendes a identificar quién hace cada petición (autenticación) y a proteger rutas usando el sistema de dependencias de FastAPI con tokens JWT. Al terminar, tu «API para tus herramientas» podrá exigir que solo un docente autenticado registre o borre notas.
Dependencias: Depends
Una dependencia es una función que FastAPI ejecuta antes de tu endpoint y cuyo resultado inyecta como argumento. Ya la usamos para la sesión de base de datos. Sirve para reutilizar lógica:
from fastapi import Depends, FastAPI
app = FastAPI()
def paginacion(skip: int = 0, limite: int = 10):
return {"skip": skip, "limite": limite}
@app.get("/notas")
def listar(pag: dict = Depends(paginacion)):
return pag
paginacion declara sus propios query params y FastAPI los expone en /docs. Cualquier endpoint que dependa de ella los hereda. Las dependencias se encadenan: una puede depender de otra. Esa es la pieza con la que construiremos la autenticación.
Hashing de contraseñas
Nunca guardes contraseñas en texto plano. Se almacena un hash irreversible con passlib:
pip install "passlib[bcrypt]"
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verificar_password(plano: str, hashed: str) -> bool:
return pwd_context.verify(plano, hashed)
Al registrar un usuario guardas hash_password(...); al iniciar sesión comparas con verificar_password(...). El hash nunca se puede revertir, así que aunque se filtre la base, las contraseñas siguen protegidas.
Tokens JWT
Tras validar usuario y contraseña, entregas un JWT (JSON Web Token): un texto firmado que el cliente reenvía en cada petición para probar quién es, sin volver a mandar la contraseña.
pip install "python-jose[cryptography]"
from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError
SECRET_KEY = "cambia-esto-por-una-clave-larga-y-secreta"
ALGORITHM = "HS256"
def crear_token(datos: dict, minutos: int = 30) -> str:
payload = datos.copy()
payload["exp"] = datetime.now(timezone.utc) + timedelta(minutes=minutos)
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
El token lleva un payload (por ejemplo el nombre del usuario en sub) y una expiración exp. Está firmado con SECRET_KEY: si alguien lo altera, la firma deja de cuadrar y se rechaza. La clave secreta debe venir de una variable de entorno, nunca escrita en el código.
Un JWT va firmado, no cifrado. Cualquiera puede leer su contenido (es Base64), así que no metas datos sensibles dentro. Su valor está en que no se puede falsificar sin la clave.
Endpoint de login
FastAPI trae OAuth2PasswordBearer, que extrae el token de la cabecera Authorization: Bearer <token> y lo documenta con un botón Authorize en /docs:
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
# Usuario de ejemplo; en real vendría de la base de datos
USUARIOS = {"profe": {"password_hash": hash_password("clave123")}}
@app.post("/login")
def login(form: OAuth2PasswordRequestForm = Depends()):
usuario = USUARIOS.get(form.username)
if not usuario or not verificar_password(form.password, usuario["password_hash"]):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Credenciales inválidas")
token = crear_token({"sub": form.username})
return {"access_token": token, "token_type": "bearer"}
OAuth2PasswordRequestForm recibe username y password como datos de formulario, el estándar OAuth2.
Proteger rutas
Creamos una dependencia que valida el token y devuelve el usuario actual. Cualquier endpoint que la incluya queda protegido:
def usuario_actual(token: str = Depends(oauth2_scheme)) -> str:
credenciales_error = HTTPException(
status.HTTP_401_UNAUTHORIZED,
"Token inválido",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
usuario = payload.get("sub")
if usuario is None:
raise credenciales_error
return usuario
except JWTError:
raise credenciales_error
@app.post("/notas")
def crear_nota(valor: float, usuario: str = Depends(usuario_actual)):
return {"valor": valor, "creada_por": usuario}
Sin un token válido, usuario_actual lanza 401 y el endpoint ni se ejecuta. Como usuario ya es el nombre verificado, puedes registrar quién hizo cada acción.
Ejemplo guiado: proteger el registro de notas
- Junta hashing, JWT,
/login,usuario_actualy unPOST /notasprotegido enmain.py. Correfastapi dev main.py. - En
/docs, llamaGET /notas(déjalo público) y luegoPOST /notassin autenticar: responde 401. - Pulsa el botón Authorize arriba, escribe
profe/clave123y autoriza. FastAPI guarda el token. - Repite
POST /notas: ahora pasa y la respuesta incluye"creada_por": "profe". - Decodifica tu token en jwt.io y verás el
suby la expiración (sin poder falsificarlo).
No subas la SECRET_KEY al repositorio ni la dejes fija en el código. Léela con os.environ["SECRET_KEY"] y mantén un .env fuera de git. Una clave filtrada deja a cualquiera firmar tokens válidos.
Tech English: authentication = quién eres; authorization = qué puedes hacer. bearer token = token al portador. claim = cada dato dentro del JWT. hash = resumen irreversible.
Ejercicios
-
Registro y login. Diseña
POST /usuarios(crea un usuario guardando el hash, nunca la contraseña) yPOST /loginque devuelva un JWT. Evalúa: ¿la contraseña nunca se devuelve ni se guarda en claro? ¿el login responde 401 con undetailclaro ante credenciales malas? -
Permisos por rol. Añade un
rolal token ("profe"o"estudiante") y una dependenciasolo_profeque rechace con 403 si el rol no es docente. ProtegeDELETE /notas/{id}con ella. Evalúa el diseño: ¿401 para no autenticado y 403 para autenticado sin permiso? Esto define quién puede modificar datos en el proyecto final.