🎯 El Desafío de la Autenticación en APIs
Imagina que tu API RAG está en producción:
- ✅ Endpoints públicos:
/api/health
,/api/rag/ask
- ⚠️ Endpoints sensibles:
/api/vectorstore/load
,/api/status
(detalles completos) - ⚠️ Endpoints administrativos:
/api/vectorstore/delete
El problema: ¿Cómo proteges los endpoints sensibles sin comprometer la performance o escalabilidad?
Requisitos de Autenticación
- 🔒 Seguro: Tokens no falsificables
- ⚡ Rápido: Validación en <5ms
- 📈 Escalable: Sin estado compartido entre instancias
- 🔄 Stateless: No requiere base de datos de sesiones
- 🌐 Estándar: Compatible con cualquier cliente HTTP
- 🔑 Rotable: Cambio de claves sin downtime
- ⏰ Temporal: Tokens con expiración automática
Opciones de Autenticación
Método | Pros | Contras | Escalabilidad |
---|---|---|---|
Session cookies | Familiar, fácil | Requiere DB/Redis | Limitada ⚠️ |
API Keys | Simple | No expira, difícil rotación | Media ⚠️ |
OAuth 2.0 | Estándar, robusto | Complejo setup | Alta ✅ |
JWT (HS256) | Stateless, rápido | Secret compartido | Alta ✅ |
JWT (RS256) | Stateless, secure | Requiere key pair | Muy Alta ✅✅ |
Nuestra elección: JWT con RS256 (RSA)
📊 La Magnitud del Problema
Desafíos Técnicos
- 🔐 Generación de Claves: ¿Cómo generar par RSA seguro?
- 📝 Claims Management: ¿Qué información incluir en el token?
- ✅ Validación: ¿Cómo validar firma sin secret compartido?
- 🔄 Distribución: ¿Cómo distribuir public key a la API?
- ⏰ Expiración: ¿Cómo manejar tokens expirados?
- 🔁 Renovación: ¿Cómo implementar refresh tokens?
- 🚫 Revocación: ¿Cómo invalidar tokens antes de expiración?
JWT vs Traditional Sessions
Traditional Sessions:
Client → API → Check session in Redis/DB (20-50ms)
↓
Response
Scaling: N servers = N connections to Redis
Cost: Redis instance + network latency
SPOF: Redis down = all auth fails
JWT (Stateless):
Client → API → Validate signature locally (<5ms)
↓
Response
Scaling: N servers = 0 shared state
Cost: CPU for validation (negligible)
SPOF: None (cada server valida independientemente)
💡 La Solución: JWT con RSA (RS256)
¿Qué es JWT?
JWT (JSON Web Token) es un estándar (RFC 7519) para tokens de acceso:
- Header: Algoritmo y tipo
- Payload: Claims (datos del usuario)
- Signature: Firma criptográfica
¿Por Qué RS256 (RSA)?
HS256 (HMAC) vs RS256 (RSA):
Aspecto | HS256 | RS256 |
---|---|---|
Algoritmo | HMAC + SHA256 (symmetric) | RSA + SHA256 (asymmetric) |
Claves | 1 secret compartido | Private + Public key |
Seguridad | Secret debe estar en todos los servers | Solo public key en API |
Compromiso | Si leak = regenerar tokens de todos | Si leak public key = no importa |
Uso | Pequeña escala, single app | Multi-service, microservices |
Nuestra elección | ❌ | ✅ RS256 |
RS256 permite:
- ✅ Private key en un solo lugar (token generation service)
- ✅ Public key en múltiples APIs (stateless validation)
- ✅ Zero trust: APIs solo validan, nunca generan tokens
🚀 Implementación Paso a Paso
1. Generación de Claves RSA
El script utils/generate_jwt_keys.sh
genera el par de claves RSA necesarias para JWT:
¿Qué hace el script?
- Genera clave privada RSA de 2048 bits con
openssl genrsa
- Extrae clave pública de la privada con
openssl rsa -pubout
- Establece permisos seguros:
- Private key:
600
(solo owner puede leer/escribir) - Public key:
644
(todos pueden leer)
- Private key:
- Guarda en directorio
keys/
Ejecutar:
cd utils
chmod +x generate_jwt_keys.sh
./generate_jwt_keys.sh
Output del script:
[INFO] Generando clave privada RSA de 2048 bits...
[SUCCESS] Clave privada generada: keys/private_key.pem
[INFO] Generando clave pública RSA...
[SUCCESS] Clave pública generada: keys/public_key.pem
[SUCCESS] Permisos de archivos configurados correctamente
Información de las claves generadas:
==================================
Clave Privada:
Archivo: keys/private_key.pem
Tamaño: 1.7K
Permisos: -rw------- (600)
Clave Pública:
Archivo: keys/public_key.pem
Tamaño: 451B
Permisos: -rw-r--r-- (644)
Notas importantes:
- La clave privada debe mantenerse segura y no compartirse
- La clave pública puede ser compartida para validación de tokens
- NO versionar private_key.pem en Git
Estructura generada:
keys/
├── private_key.pem # 🔒 Para generar tokens (NUNCA compartir)
└── public_key.pem # 🔓 Para validar tokens (OK compartir)
Formato de las claves:
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA2Z7X3Y+9QvH5xK...
-----END RSA PRIVATE KEY-----
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ...
-----END PUBLIC KEY-----
2. Generación de Tokens JWT
El script utils/generate_jwt_token.py
usa las claves RSA para generar tokens:
¿Qué hace el script?
-
Carga la clave privada desde
keys/private_key.pem
-
Construye payload con claims estándar:
-
sub
: username -
iss
: "lus-laboris-api" (issuer) -
aud
: "lus-laboris-client" (audience) -
exp
: timestamp de expiración -
iat
: timestamp de creación
-
- Firma con RS256 usando la clave privada
- Retorna token en formato JWT
Uso del script:
# Generar token (expira en 15 minutos por defecto)
python utils/generate_jwt_token.py --username admin@example.com
# Con expiración custom (1440 min = 24 horas)
python utils/generate_jwt_token.py --username admin@example.com --expiry 1440
# Con claims adicionales
python utils/generate_jwt_token.py --username admin@example.com \
--claims '{"role": "admin", "permissions": ["read", "write"]}'
# Validar token
python utils/generate_jwt_token.py --validate --token "eyJhbGc..."
Output del script:
ℹ️ Clave privada cargada desde: keys/private_key.pem
✅ Token generado exitosamente para usuario: admin@example.com
ℹ️ Token expira en: 2025-10-18 15:30:00 UTC (15 minutos)
🔑 Token JWT Generado Exitosamente!
==================================================
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWI...
==================================================
📖 Instrucciones de Uso:
1. Usa este token en el header Authorization:
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI...
2. Prueba el token con:
python generate_jwt_token.py --validate --token "eyJhbGc..."
Estructura de un Token JWT:
El token tiene 3 partes separadas por .
:
[Header].[Payload].[Signature]
Header (base64):
{
"alg": "RS256",
"typ": "JWT"
}
Payload (base64):
{
"sub": "admin@example.com", // Usuario
"iss": "lus-laboris-api", // Emisor
"aud": "lus-laboris-client", // Audiencia
"exp": 1729271800, // Expira (Unix timestamp)
"iat": 1729268200 // Creado (Unix timestamp)
}
Signature:
RS256(header + "." + payload, private_key) // Firma RSA
Claims importantes:
- sub: Identifica al usuario
- iss/aud: Previenen uso del token en otros sistemas
- exp: Auto-expiración (no requiere revocación manual)
- iat: Para auditoría
3. Validación en FastAPI (JWTValidator)
La clase JWTValidator
en jwt_handler.py
se encarga de validar tokens:
¿Qué hace JWTValidator
?
-
Carga la clave pública al inicializar
- Lee desde
settings.api_jwt_public_key_path
- Típicamente:
keys/public_key.pem
- Falla temprano si el archivo no existe
- Lee desde
-
Método
validate_token()
- El corazón de la validación- Decodifica el token usando la public key
- Valida signature con algoritmo RS256
- Verifica claims: exp, iat, iss, aud
- Retorna payload si todo es válido
- Lanza
ValueError
si algo falla
-
Método
is_token_valid()
- Helper no-exception- Retorna
True
si válido,False
si inválido - No lanza excepciones (para checks silenciosos)
- Retorna
Validaciones automáticas:
- ✅ Expiration (
exp
): Token no expirado - ✅ Issued At (
iat
): Token ya fue emitido (no futuro) - ✅ Issuer (
iss
): Coincide consettings.api_jwt_iss
- ✅ Audience (
aud
): Coincide consettings.api_jwt_aud
- ✅ Signature: Firmado con la private key correcta
Errores manejados:
-
jwt.ExpiredSignatureError
→ "Token has expired" -
jwt.InvalidAudienceError
→ "Invalid audience" -
jwt.InvalidIssuerError
→ "Invalid issuer" -
jwt.InvalidTokenError
→ "Invalid token"
Performance:
- Validación completa: <5ms (CPU-bound, muy rápido)
- No requiere llamadas externas (DB, Redis, etc.)
4. Security Dependencies para FastAPI
El archivo src/lus_laboris_api/api/auth/security.py
proporciona las funciones de autenticación:
¿Qué contiene security.py
?
-
SecurityManager class
- Centraliza la lógica de autenticación
- Valida tokens JWT
- Maneja errores de autenticación
-
get_current_user()
- Dependency que requiere JWT válido
- Extrae payload del token
- Retorna información del usuario
-
optional_auth()
- Dependency con autenticación opcional
- Retorna
dict
si hay token válido - Retorna
None
si no hay token (no bloquea acceso)
-
require_admin()
- Dependency para operaciones administrativas
- Requiere token JWT válido
-
require_vectorstore_write() y require_vectorstore_read()
- Dependencies para operaciones del vectorstore
- Requieren token JWT válido
Uso en endpoints:
from ..auth.security import get_current_user, optional_auth, require_admin
# Endpoint protegido (requiere JWT)
@router.get("/protected")
async def protected_endpoint(user: dict = Depends(get_current_user)):
return {"message": f"Hello {user['sub']}"}
# Endpoint con auth opcional
@router.get("/health/detailed")
async def detailed_health(user: dict | None = Depends(optional_auth)):
if user:
return {"status": "healthy", "details": "...", "user": user['sub']}
else:
return {"status": "healthy"}
# Endpoint admin (requiere JWT)
@router.delete("/collections/{name}")
async def delete_collection(name: str, user: dict = Depends(require_admin)):
return {"deleted_by": user['sub']}
5. Swagger UI Integration
La autenticación JWT se integra automáticamente en Swagger UI gracias a HTTPBearer
:
Flujo en Swagger:
-
Acceder a Swagger UI:
http://localhost:8000/docs
- Click en "Authorize" 🔓 (botón en la parte superior)
- Ingresar token: Pegar el token JWT en el campo "Value"
- Click "Authorize": El token se guarda en la sesión
- Probar endpoints: Ahora puedes llamar endpoints protegidos
Características:
- ✅ Auto-detecta endpoints con
Depends(get_current_user)
- ✅ Candado visible 🔒 en endpoints protegidos
- ✅ Botón unlock para ingresar token
- ✅ Persistencia: Token se mantiene mientras Swagger esté abierto
- ✅ Logout: Click en "Logout" para borrar token
Ejemplo visual en Swagger:
GET /api/rag/ask [Sin candado] ← Público
GET /api/health [Sin candado] ← Público
GET /api/status 🔒 [Authorize] ← Protegido
DELETE /api/vectorstore/delete 🔒 [Authorize] ← Protegido
🔐 Integración con GCP Secret Manager
1. Almacenar Public Key en Secret Manager
# Crear secret con la public key
gcloud secrets create jwt-public-key \
--data-file=keys/public_key.pem \
--project=tu-proyecto-gcp \
--replication-policy="automatic"
# Verificar
gcloud secrets versions list jwt-public-key --project=tu-proyecto-gcp
2. Montar Secret en Cloud Run
En el deployment de Cloud Run:
gcloud run deploy lus-laboris-api \
--image=tu-imagen \
--update-secrets="/app/secrets/jwt/public_key.pem=jwt-public-key:latest" \
--set-env-vars="API_JWT_PUBLIC_KEY_PATH=/app/secrets/jwt/public_key.pem"
3. GitHub Actions Workflow
- name: Update JWT Public Key Secret
run: |
echo "${JWT_PUBLIC_KEY}" | \
gcloud secrets versions add jwt-public-key \
--data-file=- \
--project=${GCP_PROJECT_ID}
echo "✅ JWT public key secret updated"
🎯 Casos de Uso Reales
Para APIs Públicas con Endpoints Protegidos:
"Quiero que /health sea público pero /status sea protegido"
Solución:
@router.get("/health")
async def health():
"""Public - no auth"""
return {"status": "healthy"}
@router.get("/status")
async def status(user: dict = Depends(get_current_user)):
"""Protected - JWT required"""
return {
"status": "healthy",
"details": {...}, # Info sensible
"requested_by": user["sub"]
}
Para Integración con Aplicaciones Externas:
"Otra aplicación necesita cargar datos al vectorstore"
Solución:
# Generar token para la aplicación externa
python generate_jwt_token.py generate \
--username external-app@company.com \
--expiry 10080 # 7 días
# Compartir token (de forma segura)
# La app externa puede hacer requests:
curl -X POST http://api/vectorstore/load \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{"filename": "data.json"}'
Para Diferentes Niveles de Acceso:
"Admin puede delete, user puede read"
Solución:
def require_admin(user: dict = Depends(get_current_user)) -> dict:
"""Dependency que requiere rol admin"""
if user.get("role") != "admin":
raise HTTPException(status_code=403, detail="Admin access required")
return user
@router.delete("/collections/{name}")
async def delete_collection(
name: str,
admin: dict = Depends(require_admin)
):
"""Solo admins pueden eliminar"""
...
Para Debugging y Testing:
"Quiero probar endpoints protegidos en desarrollo"
Solución:
# Generar token de desarrollo (expira en 24 horas)
python generate_jwt_token.py generate \
--username dev@localhost \
--expiry 1440 > dev_token.txt
# Usar en requests
export TOKEN=$(cat dev_token.txt)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/status
🚀 El Impacto Transformador
Antes de JWT:
- 🔑 Session management: Redis/DB requerido para sesiones
- 📊 Scaling issues: N servers = N connections to session store
- ⏱️ Latency: 20-50ms para check session
- 💰 Cost: Redis instance ($50-100/mes)
- 🚨 SPOF: Redis down = all auth fails
Después de JWT (RS256):
- 🔑 Stateless: Cero dependencias externas
- 📊 Infinite scaling: N servers = 0 shared state
- ⏱️ Ultra-fast: <5ms validation (CPU-bound)
- 💰 Zero cost: CPU cost negligible
- 🚨 No SPOF: Cada server valida independientemente
Métricas de Mejora:
Aspecto | Sin JWT | Con JWT (RS256) | Mejora |
---|---|---|---|
Latency de validación | 20-50ms | <5ms | -90% |
External dependencies | Redis/DB | None | -100% |
Cost (auth infra) | $50-100/mes | $0 | -100% |
Scaling complexity | High | Zero | N/A |
SPOF risk | Yes | No | N/A |
💡 Lecciones Aprendidas
1. RS256 > HS256 para Multi-Service
Si tu API se comunica con otros servicios, RS256 es mejor porque solo necesitas compartir la public key (no sensitive).
2. Expiry Time es Crítico
- Corto (15-60 min): Más seguro, pero requiere refresh frecuente
- Largo (24 horas+): Más convenient, pero mayor ventana de compromiso
- Nuestro sweet spot: 60 minutos para API calls, 7 días para batch jobs
3. Claims Mínimos son Mejores
Solo incluir en el payload lo que realmente necesitas validar. No uses JWT como session storage.
4. Public Key en Secret Manager
Aunque la public key no es sensible, almacenarla en Secret Manager facilita rotación sin re-deploy.
5. optional_auth
es Poderoso
Permite endpoints que dan más info si estás autenticado, pero no bloquean acceso público.
6. Swagger UI con JWT es UX++
El botón "Authorize" en Swagger UI hace testing de endpoints protegidos trivial.
🔐 Seguridad Best Practices
1. Never Version Private Key
# .gitignore
keys/private_key.pem
*.pem
!public_key.pem # Public key is safe
2. Secure Key Permissions
chmod 600 keys/private_key.pem # Owner read/write only
chmod 644 keys/public_key.pem # Read for everyone (safe)
3. Rotate Keys Periodically
# Generate new key pair
./generate_jwt_keys.sh --force
# Deploy new public key to API
gcloud secrets versions add jwt-public-key --data-file=keys/public_key.pem
# Gradually phase out old tokens (based on expiry)
4. Use Short-Lived Tokens
# For interactive users: 15-60 minutes
token = generator.generate_token(username, expiry_minutes=60)
# For service accounts: 7 days max
token = generator.generate_token(username, expiry_minutes=10080)
5. Validate All Standard Claims
# Always verify: exp, iat, iss, aud
payload = jwt.decode(
token,
public_key,
options={
"verify_exp": True,
"verify_iat": True,
"verify_aud": True,
"verify_iss": True,
}
)
🎯 El Propósito Más Grande
JWT con RS256 no es solo autenticación - es libertad arquitectural. Al eliminar la necesidad de estado compartido:
- 🚀 Scaling: Cada instancia de la API es independiente
- 🌐 Multi-region: Tokens válidos en cualquier región
- 🔄 Microservices: Diferentes servicios validan con la misma public key
- 💰 Cost: Zero infrastructure para auth
- ⚡ Performance: Validación local ultra-rápida
- 🔒 Security: Public key leak = no problem
Estamos construyendo una API que escala infinitamente sin necesidad de coordinación entre instancias, sin single points of failure, y sin costo adicional de infraestructura.
🔗 Recursos y Enlaces
Repositorio del Proyecto
- GitHub: lus-laboris-py
Documentación Técnica
-
JWT Handler:
src/lus_laboris_api/api/auth/jwt_handler.py
-
Security Dependencies:
src/lus_laboris_api/api/auth/security.py
-
Generate Keys Script:
utils/generate_jwt_keys.sh
-
Generate Token Script:
utils/generate_jwt_token.py
Recursos Externos
- JWT.io: jwt.io - Decode y debug tokens
- RFC 7519 (JWT): tools.ietf.org/html/rfc7519
- PyJWT Docs: pyjwt.readthedocs.io
- RSA Cryptography: cryptography.io
Próximo Post: LLPY-11 - Terraform: Infraestructura como Código
En el siguiente post exploraremos cómo gestionar toda la infraestructura de GCP con Terraform, desde VMs para Qdrant hasta Cloud Run para la API, con módulos reutilizables y CI/CD automatizado.
Top comments (0)