crowdata / app /auth /config.py
YOSOYYONOSOYOTRO's picture
Upload folder using huggingface_hub
83bdb4a verified
Raw
History Blame Contribute Delete
5.94 kB
from fastapi_users.authentication import (
AuthenticationBackend,
BearerTransport,
CookieTransport,
JWTStrategy,
)
from app.config import get_settings
from typing import Optional, Any
import jwt
from jwt import PyJWK
from datetime import datetime, timedelta
settings = get_settings()
# Bearer transport (para APIs programáticas / mobile)
bearer_transport = BearerTransport(tokenUrl="api/auth/jwt/login")
# Cookie transport (para navegadores - HttpOnly, Secure, SameSite=Lax)
cookie_transport = CookieTransport(
cookie_name="cd_token",
cookie_max_age=settings.access_token_expire_minutes * 60,
cookie_secure=not settings.debug, # Secure=True en prod, False en dev local
cookie_httponly=True,
cookie_samesite="lax",
)
# ─── Custom Dual JWT Strategy ───
# Firma nuevos tokens con RS256 (private key)
# Verifica: intenta RS256 (public key) → si falla, fallback a HS256 (legacy secret)
class DualJWTStrategy(JWTStrategy):
"""JWT Strategy que soporta verificación dual: RS256 (nuevo) + HS256 (legacy)."""
def __init__(self):
# Cargar claves
self._private_key = self._load_private_key()
self._public_key = self._load_public_key()
self._legacy_secret = settings.jwt_legacy_secret_key or settings.secret_key
self._algorithm = settings.jwt_algorithm
self._legacy_algorithm = settings.jwt_legacy_algorithm
self._lifetime_seconds = settings.access_token_expire_minutes * 60
self._key_id = settings.jwt_key_id
# Initialize base class with required params (using legacy secret for base compat)
super().__init__(
secret=self._legacy_secret,
lifetime_seconds=self._lifetime_seconds,
token_audience=["fastapi-users:auth"],
algorithm=self._algorithm,
public_key=self._public_key,
)
def _load_private_key(self) -> Optional[str]:
"""Cargar clave privada RS256 desde config o archivo."""
if settings.jwt_private_key:
return settings.jwt_private_key
# Fallback: leer archivo
import os
key_path = os.path.join(os.path.dirname(__file__), '..', '..', 'private_key.pem')
if os.path.exists(key_path):
with open(key_path, 'r') as f:
return f.read()
return None
def _load_public_key(self) -> Optional[str]:
"""Cargar clave pública RS256 desde config o archivo."""
if settings.jwt_public_key:
return settings.jwt_public_key
# Fallback: leer archivo
import os
key_path = os.path.join(os.path.dirname(__file__), '..', '..', 'public_key.pem')
if os.path.exists(key_path):
with open(key_path, 'r') as f:
return f.read()
return None
async def write_token(self, user) -> str:
"""Firmar token con RS256 (nueva clave privada). Fallar si no hay clave."""
# Extraer ID del usuario (fastapi-users pasa el objeto User)
user_id = str(user.id)
data = {"sub": user_id, "aud": self.token_audience}
if not self._private_key:
raise RuntimeError(
"JWT_PRIVATE_KEY no configurada. Configure RS256 keys para firmar tokens. "
"No se permite fallback silencioso a HS256."
)
# Agregar kid en header para rotación de claves
headers = {"kid": self._key_id}
return jwt.encode(
{**data, "exp": datetime.utcnow() + timedelta(seconds=self._lifetime_seconds)},
self._private_key,
algorithm=self._algorithm,
headers=headers,
)
async def read_token(self, token: Optional[str], user_manager) -> Optional[Any]:
"""Verificar token: solo RS256. Legacy HS256 solo si jwt_legacy_enabled=True."""
if token is None:
return None
# Intentar RS256 (nuevo)
if self._public_key:
try:
data = jwt.decode(
token,
self._public_key,
algorithms=[self._algorithm],
audience=self.token_audience,
)
user_id = data.get("sub")
if user_id is None:
return None
parsed_id = user_manager.parse_id(user_id)
return await user_manager.get(parsed_id)
except jwt.PyJWTError:
pass # Fallar a legacy si habilitado
# Legacy HS256 SOLO si explícitamente habilitado
if settings.jwt_legacy_enabled and self._legacy_secret:
try:
data = jwt.decode(
token,
self._legacy_secret,
algorithms=[self._legacy_algorithm],
audience=self.token_audience,
)
user_id = data.get("sub")
if user_id is None:
return None
parsed_id = user_manager.parse_id(user_id)
return await user_manager.get(parsed_id)
except jwt.PyJWTError:
pass
# Si llegamos aquí, token inválido
return None
async def destroy_token(self, token: str, user) -> None:
"""JWT es stateless, no hay nada que destruir server-side."""
pass
def get_jwt_strategy() -> DualJWTStrategy:
return DualJWTStrategy()
# Dual auth backend: soporta AMBOS transports (header Authorization + cookie)
auth_backend = AuthenticationBackend(
name="jwt",
transport=bearer_transport, # primary para compatibilidad
get_strategy=get_jwt_strategy,
)
# Segundo backend solo para cookies (se usa en login para setear cookie)
cookie_auth_backend = AuthenticationBackend(
name="jwt-cookie",
transport=cookie_transport,
get_strategy=get_jwt_strategy,
)