CreaRack-SL

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:

  1. Si hay token cacheado en _cached_token, devolverlo.
  2. Si LOCAL_TOKEN_FILE existe, descifrarlo y cachearlo.
  3. Si descifrado falla (fichero corrupto), regenerar + persistir.
  4. Si no existe, generar con secrets.token_urlsafe(32), persistir en %APPDATA%/CreaRackAgent/local_token.enc (cifrado DPAPI), cachear.
  5. 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_manager exists AND
  • auth_manager.saas_url is set AND
  • auth_manager.agent_id is 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):

  1. Security headers (CSP + X-Frame):

    • Content-Security-Policy: valor de csp_frame_ancestors
    • X-Frame-Options: ALLOWALL
  2. 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"}.
  3. CORS (CORSMiddleware de FastAPI, capa externa):

    • allow_origins: resultado de allowed_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_token para 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-token en 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

TestQué verifica
test_token_generated_and_persistedGeneración, persistencia en fichero, lectura posterior (cache + disco).
test_verify_tokenverify_local_token(token) correcto, incorrecto, None, "".
test_auth_required_only_when_linkedauth_required() solo True si saas_url + agent_id.
test_extract_bearerParseo de Authorization (case-insensitive, trim spaces).
test_exempt_paths_cover_bootstrapEXEMPT_PATHS incluye /info, /health, /terminal/ui, /saas/configure; excluye acciones sensibles.
test_corrupt_store_regeneratesDPAPI 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 (en install_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]]