Servicio local_token · Generación y validación del token de auth del Agent
Resumen
Módulo terminal/agent/core/local_token.py (177 líneas). Encargado de generar, persistir y validar el token local que el Agent exige en toda su API HTTP y WebSocket. Token opaco (secrets.token_urlsafe(32)), persistido cifrado en DPAPI, verificado en tiempo constante.
Interfaz pública
get_or_create_local_token() -> str
Devuelve el token local, generándolo y persistiéndolo la primera vez.
Lógica:
- Si hay token cacheado en
_cached_token, devolverlo. - Si
LOCAL_TOKEN_FILEexiste, descifrarlo y cachearlo. - Si descifrado falla (fichero corrupto), regenerar + persistir.
- Si no existe, generar con
secrets.token_urlsafe(32), persistir en%APPDATA%/CreaRackAgent/local_token.enc(cifrado DPAPI), cachear. - Si persistencia falla, usar token en memoria (sin fallar).
Returns: Token de 43-44 caracteres base64url.
Manejo de fallos: Sin excepciones al caller; logger warnings para ficheros dañados o sin permiso de escritura.
verify_local_token(candidate: str | None) -> bool
Compara el candidato con el token local en tiempo constante.
Lógica:
if not candidate:
return False
return hmac.compare_digest(candidate, get_or_create_local_token())
Inmunidad a timing attacks: hmac.compare_digest() siempre tarda lo mismo, independientemente de dónde falle la comparación.
auth_required(auth_manager) -> bool
Determina si la API local debe exigir el token Bearer.
Lógica: True solo si el Agent está vinculado a un SaaS:
auth_managerexists ANDauth_manager.saas_urlis set ANDauth_manager.agent_idis set
Pre-activación: auth_required() = False → sin exigencia, compat con .exe viejos.
extract_bearer(headers) -> str | None
Parsea el token del header Authorization: Bearer <token>.
Lógica: Busca el prefijo "Bearer " (case-insensitive en la clave), extrae y trimea.
Ejemplos:
{"authorization": "Bearer abc123"}→"abc123"{"Authorization": "Bearer xyz "}→"xyz"{"authorization": "Basic abc"}→None{}→None
allowed_origins_for(auth_manager, port: int) -> list[str]
Calcula los origins CORS permitidos.
Lógica:
- Siempre:
["http://127.0.0.1:PORT", "http://localhost:PORT"] - Si vinculado a SaaS: añadir
<saas_url>(ej."https://crearack.com") - Pre-activación: retornar
["*"](sin SaaS conocido)
Ejemplo:
allowed_origins_for(_FakeAuth(saas_url="https://crearack.com"), 7777)
# → ["http://127.0.0.1:7777", "http://localhost:7777", "https://crearack.com"]
install_local_security(app, get_auth_manager, port: int, csp_frame_ancestors: str) -> None
Instala en la app FastAPI los security headers, gate de auth local y CORS (Fase 3).
Middlewares (en orden):
-
Security headers (CSP + X-Frame):
Content-Security-Policy: valor decsp_frame_ancestorsX-Frame-Options: ALLOWALL
-
Gate Bearer (require_local_token):
- Si
method == OPTIONS→ pasar (preflight CORS). - Si
path in EXEMPT_PATHS→ pasar. - Si
not auth_required()→ pasar (pre-activación). - Si
verify_local_token(extract_bearer(headers))→ pasar. - Else → 401
{"error": "Local auth token required"}.
- Si
-
CORS (CORSMiddleware de FastAPI, capa externa):
allow_origins: resultado deallowed_origins_for().allow_credentials: False.allow_methods: ["*"].allow_headers: ["*"].
Importancia del orden: CORS es la capa más EXTERNA para que aplique preflight y añada headers CORS también a los 401 del gate.
Rutas exentas (EXEMPT_PATHS)
El gate Bearer no se aplica a:
EXEMPT_PATHS = frozenset({
"/", # Health check, sin datos sensibles
"/info", # agent_id, agent_version (bootstrap frontend)
"/health", # Status local, sin acciones
"/check", # Health probe
"/favicon.ico", # Recurso estático
"/terminal/ui", # HTML del iframe (no puede llevar headers)
"/saas/configure", # Flujo de activación: recibe el SaaS URL
"/saas/status", # Comprueba estado de vinculación
"/saas/connect", # Finaliza activación (SaaS aún no tiene token)
})
Razón: Bootstrap del frontend (descubrir agent_id, configurar SaaS) debe funcionar ANTES de que el token esté depositado en el servidor.
Persistencia: DPAPI
El token se almacena en %APPDATA%/CreaRackAgent/local_token.enc cifrado con DPAPI (Data Protection API de Windows).
Módulo relacionado: [[entity—agent—service—local-token]], que proporciona encrypt_secret() y decrypt_secret().
Ventajas:
- Vinculado a la sesión del usuario Windows actual → no reutilizable en otro PC o usuario.
- Gestión automática de claves (OS maneja la Master Key).
- Mismo mecanismo que
credentials.enc(credenciales SaaS).
Fallos tolerables:
- Fichero corrupto → regenerar token (loss-of-connectivity temporal; el SaaS aún lo custodia).
- Sin permiso de escritura → usar token en memoria (re-generado en cada reinicio, pero compat).
Caching en memoria
Variable global _cached_token: str | None.
- Primera llamada a
get_or_create_local_token(): genera/carga del almacén, cachea. - Llamadas posteriores: devuelven el cacheado sin tocar disco (perf).
- Tests: monkeypatch de
_cached_tokenpara simular reinicios (patrón s124).
Seguridad adicional
- Rate limiting: El gate Bearer es único, sin límite de intentos. Se recomienda un RateLimitMiddleware en 401s a nivel del proxy/nginx.
- Rotación: Token se genera una sola vez al activar el Agent; no hay rotación automática. Si se comprometiera, requeriría re-activación manual (feature futura).
- Auditoría: POST
/api/agent/register-local-tokenen el SaaS puede loguearse para auditar descargas de token.
Flujo de uso
1. Agent arranca (main.py):
→ install_local_security() instala middlewares
→ get_auth_manager() devuelve auth_manager (puede ser None si pre-activación)
→ middleware require_local_token verifica cada request
2. Conecta a SaaS (connector.py):
→ _register_local_token() → POST /api/agent/register-local-token
→ SaaS custodia el token
3. Frontend (Fase 2) en navegador:
→ GET /api/agent/local-token (auténtico contra SaaS)
→ SaaS devuelve token (solo a sesión del tenant dueño)
→ Frontend inyecta en postMessage SET_AUTH_TOKEN → terminal.html
→ terminal.html manda {"action":"auth","token":...} en WS open
4. WS consola recibe token:
→ routes/terminal.py valida primer mensaje
→ verify_local_token() verifica
→ Sesión abierta (o close 4401)
Testing
Archivo: tests/agent/test_agent_local_auth.py
| Test | Qué verifica |
|---|---|
test_token_generated_and_persisted | Generación, persistencia en fichero, lectura posterior (cache + disco). |
test_verify_token | verify_local_token(token) correcto, incorrecto, None, "". |
test_auth_required_only_when_linked | auth_required() solo True si saas_url + agent_id. |
test_extract_bearer | Parseo de Authorization (case-insensitive, trim spaces). |
test_exempt_paths_cover_bootstrap | EXEMPT_PATHS incluye /info, /health, /terminal/ui, /saas/configure; excluye acciones sensibles. |
test_corrupt_store_regenerates | DPAPI corrupta → regenera token correctamente. |
Ejecución: ✓ Verdes en Docker. Patrón de carga por ruta (s124): no importa como package, carga como módulo directo.
Dependencias
hmac,secrets,pathlib(stdlib)core.crypto:encrypt_secret(),decrypt_secret()(DPAPI)config:INSTALL_DIR(ruta%APPDATA%/CreaRackAgent)fastapi:Request,JSONResponse,CORSMiddleware(eninstall_local_security())
Véase también
- [[feature—agent—fase-3-auth-local]]
- [[entity—agent—service—connector-register-local-token]]
- [[entity—agent—service—terminal-ws-auth]]
- [[concept—security—data-protection-api]]
- [[concept—security—constant-time-comparison]]