11. Autenticación y seguridad
🔐 11.1 Introducción a la autenticación en APIs
Section titled “🔐 11.1 Introducción a la autenticación en APIs”Tipos de autenticación
Section titled “Tipos de autenticación”| Tipo | Descripción | Uso común |
|---|---|---|
| API Key | Clave simple en header | APIs internas |
| Basic Auth | Usuario:password en base64 | APIs simples |
| Bearer Token | Token en header Authorization | APIs modernas |
| JWT | Token firmado con datos | APIs REST |
| OAuth2 | Protocolo estándar | Login con terceros |
Flujo de autenticación típico
Section titled “Flujo de autenticación típico”- Usuario envía credenciales (email/password)
- Servidor valida y genera token
- Cliente guarda el token
- Cliente envía token en cada petición
- Servidor valida token y procesa petición
Dependencias necesarias
Section titled “Dependencias necesarias”# Instalar dependencias para autenticaciónpip install python-jose[cryptography] # Para JWTpip install passlib[bcrypt] # Para hashear passwordspip install python-multipart # Para formularios🔑 11.2 Tokens JWT
Section titled “🔑 11.2 Tokens JWT”Qué es JWT
Section titled “Qué es JWT”JWT (JSON Web Token) es un estándar para crear tokens de acceso que contienen información del usuario de forma segura.
| Parte | Contenido |
|---|---|
| Header | Tipo de token y algoritmo |
| Payload | Datos del usuario (claims) |
| Signature | Firma para verificar autenticidad |
Crear y verificar tokens
Section titled “Crear y verificar tokens”# app/core/security.pyfrom datetime import datetime, timedeltafrom jose import JWTError, jwtfrom passlib.context import CryptContext
# ConfiguraciónSECRET_KEY = "tu-clave-secreta-muy-larga-y-segura"ALGORITHM = "HS256"ACCESS_TOKEN_EXPIRE_MINUTES = 30
# Contexto para hashear passwordspwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def crear_token(data: dict, expires_delta: timedelta = None): """Crea un token JWT.""" 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
def verificar_token(token: str): """Verifica y decodifica un token JWT.""" try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) return payload except JWTError: return None
def verificar_password(password_plano: str, password_hash: str): """Verifica si el password coincide con el hash.""" return pwd_context.verify(password_plano, password_hash)
def hashear_password(password: str): """Genera el hash de un password.""" return pwd_context.hash(password)Ejemplo: Generar token
Section titled “Ejemplo: Generar token”from app.core.security import crear_token, hashear_passwordfrom datetime import timedelta
# Crear token para un usuariotoken = crear_token( data={"sub": "usuario@email.com", "rol": "admin"}, expires_delta=timedelta(hours=24))print(token)# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# Hashear passwordpassword_hash = hashear_password("mi_password_seguro")print(password_hash)# $2b$12$...🛡️ 11.3 OAuth2 con FastAPI
Section titled “🛡️ 11.3 OAuth2 con FastAPI”Configurar OAuth2
Section titled “Configurar OAuth2”FastAPI incluye soporte integrado para OAuth2 con el esquema “password flow”.
# app/core/auth.pyfrom fastapi import Depends, HTTPException, statusfrom fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestFormfrom jose import JWTError, jwtfrom pydantic import BaseModelfrom typing import Optional
from app.core.security import SECRET_KEY, ALGORITHM, verificar_token
# Esquema OAuth2 - indica dónde obtener el tokenoauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
class TokenData(BaseModel): email: Optional[str] = None rol: Optional[str] = None
async def obtener_usuario_actual(token: str = Depends(oauth2_scheme)): """Dependencia que extrae el usuario del token.""" credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="No se pudieron validar las credenciales", headers={"WWW-Authenticate": "Bearer"}, )
payload = verificar_token(token) if payload is None: raise credentials_exception
email: str = payload.get("sub") if email is None: raise credentials_exception
return TokenData(email=email, rol=payload.get("rol"))Endpoint de login
Section titled “Endpoint de login”# app/routers/auth.pyfrom fastapi import APIRouter, Depends, HTTPException, statusfrom fastapi.security import OAuth2PasswordRequestFormfrom datetime import timedelta
from app.core.security import ( crear_token, verificar_password, ACCESS_TOKEN_EXPIRE_MINUTES)from app.db.database import get_dbfrom app.crud.usuario import obtener_usuario_por_emailfrom sqlalchemy.orm import Session
router = APIRouter(prefix="/auth", tags=["Autenticación"])
@router.post("/login")async def login( form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): """Endpoint de login que devuelve un token JWT.""" # Buscar usuario usuario = obtener_usuario_por_email(db, form_data.username)
if not usuario: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Email o contraseña incorrectos" )
# Verificar password if not verificar_password(form_data.password, usuario.password_hash): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Email o contraseña incorrectos" )
# Crear token access_token = crear_token( data={"sub": usuario.email, "rol": "usuario"}, expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) )
return { "access_token": access_token, "token_type": "bearer" }🚧 11.4 Protección de rutas
Section titled “🚧 11.4 Protección de rutas”Rutas protegidas básicas
Section titled “Rutas protegidas básicas”# app/routers/usuarios.pyfrom fastapi import APIRouter, Depends, HTTPExceptionfrom app.core.auth import obtener_usuario_actual, TokenDatafrom typing import List
router = APIRouter(prefix="/usuarios", tags=["Usuarios"])
# Ruta protegida - requiere autenticación@router.get("/perfil")async def obtener_perfil(usuario_actual: TokenData = Depends(obtener_usuario_actual)): """Obtiene el perfil del usuario autenticado.""" return { "email": usuario_actual.email, "rol": usuario_actual.rol }
# Ruta protegida con datos del usuario@router.get("/mis-pedidos")async def mis_pedidos(usuario_actual: TokenData = Depends(obtener_usuario_actual)): """Lista los pedidos del usuario autenticado.""" return { "usuario": usuario_actual.email, "pedidos": [] }Verificar roles
Section titled “Verificar roles”# app/core/auth.py (agregar)from functools import wraps
def requiere_rol(roles_permitidos: list): """Dependencia que verifica si el usuario tiene el rol requerido.""" async def verificar_rol(usuario: TokenData = Depends(obtener_usuario_actual)): if usuario.rol not in roles_permitidos: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="No tienes permisos para acceder a este recurso" ) return usuario return verificar_rol
# Dependencias predefinidasrequiere_admin = requiere_rol(["admin"])requiere_moderador = requiere_rol(["admin", "moderador"])# app/routers/admin.pyfrom fastapi import APIRouter, Dependsfrom app.core.auth import requiere_admin, TokenData
router = APIRouter(prefix="/admin", tags=["Administración"])
# Solo administradores pueden acceder@router.get("/usuarios")async def listar_todos_usuarios(admin: TokenData = Depends(requiere_admin)): """Lista todos los usuarios (solo admin).""" return {"mensaje": "Lista de usuarios", "admin": admin.email}
@router.delete("/usuarios/{usuario_id}")async def eliminar_usuario( usuario_id: int, admin: TokenData = Depends(requiere_admin)): """Elimina un usuario (solo admin).""" return {"mensaje": f"Usuario {usuario_id} eliminado por {admin.email}"}🔒 11.5 Manejo seguro de contraseñas
Section titled “🔒 11.5 Manejo seguro de contraseñas”Nunca guardar passwords en texto plano
Section titled “Nunca guardar passwords en texto plano”# MAL - Nunca hacer estousuario = { "email": "ana@email.com", "password": "mi_password" # ❌ Texto plano}
# BIEN - Siempre hashearfrom passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
usuario = { "email": "ana@email.com", "password_hash": pwd_context.hash("mi_password") # ✅ Hash}Validar fortaleza del password
Section titled “Validar fortaleza del password”# app/schemas/usuario.pyfrom pydantic import BaseModel, validatorimport re
class UsuarioRegistro(BaseModel): nombre: str email: str password: str
@validator('password') def password_fuerte(cls, v): if len(v) < 8: raise ValueError('El password debe tener al menos 8 caracteres') if not re.search(r'[A-Z]', v): raise ValueError('El password debe tener al menos una mayúscula') if not re.search(r'[a-z]', v): raise ValueError('El password debe tener al menos una minúscula') if not re.search(r'[0-9]', v): raise ValueError('El password debe tener al menos un número') return vEndpoint de registro seguro
Section titled “Endpoint de registro seguro”# app/routers/auth.pyfrom pydantic import BaseModel, EmailStr, Field
class RegistroRequest(BaseModel): nombre: str = Field(..., min_length=2) email: EmailStr password: str = Field(..., min_length=8)
@router.post("/registro", status_code=201)async def registrar(datos: RegistroRequest, db: Session = Depends(get_db)): """Registra un nuevo usuario.""" # Verificar email único if obtener_usuario_por_email(db, datos.email): raise HTTPException(status_code=400, detail="Email ya registrado")
# Crear usuario con password hasheado from app.core.security import hashear_password
nuevo_usuario = Usuario( nombre=datos.nombre, email=datos.email, password_hash=hashear_password(datos.password) ) db.add(nuevo_usuario) db.commit() db.refresh(nuevo_usuario)
return {"mensaje": "Usuario registrado", "email": nuevo_usuario.email}💡 11.6 Ejemplo completo de autenticación
Section titled “💡 11.6 Ejemplo completo de autenticación”Estructura del proyecto
Section titled “Estructura del proyecto”app/├── core/│ ├── config.py # Configuración│ ├── security.py # JWT y passwords│ └── auth.py # Dependencias de auth├── routers/│ ├── auth.py # Login y registro│ └── usuarios.py # Rutas protegidas├── db/│ ├── database.py│ └── models.py└── main.pymain.py con autenticación
Section titled “main.py con autenticación”# app/main.pyfrom fastapi import FastAPIfrom app.db.database import engine, Basefrom app.routers import auth, usuarios
Base.metadata.create_all(bind=engine)
app = FastAPI( title="API con Autenticación", description="API segura con JWT", version="1.0.0")
# Routersapp.include_router(auth.router)app.include_router(usuarios.router)
@app.get("/")def inicio(): return {"mensaje": "API funcionando"}Flujo completo de uso
Section titled “Flujo completo de uso”# 1. REGISTRAR USUARIO# POST /auth/registro# Body: {"nombre": "Ana", "email": "ana@email.com", "password": "MiPassword123"}# Respuesta: {"mensaje": "Usuario registrado", "email": "ana@email.com"}
# 2. LOGIN# POST /auth/login# Body (form-data): username=ana@email.com, password=MiPassword123# Respuesta: {"access_token": "eyJhbGci...", "token_type": "bearer"}
# 3. ACCEDER A RUTA PROTEGIDA# GET /usuarios/perfil# Header: Authorization: Bearer eyJhbGci...# Respuesta: {"email": "ana@email.com", "rol": "usuario"}
# 4. SIN TOKEN -> Error 401# GET /usuarios/perfil# (sin header Authorization)# Respuesta: {"detail": "Not authenticated"}Probar en Swagger UI
Section titled “Probar en Swagger UI”# 1. Ir a http://localhost:8000/docs# 2. Ejecutar POST /auth/registro para crear usuario# 3. Ejecutar POST /auth/login con las credenciales# 4. Copiar el access_token de la respuesta# 5. Hacer clic en el botón "Authorize" (candado)# 6. Pegar el token en el campo "Value"# 7. Ahora puedes acceder a rutas protegidas📝 Resumen
Section titled “📝 Resumen”
🐝