Volver a la wiki

Servicio de autenticación Agent en frontend (`agent_auth.js`)

Propósito

Recuperar y cachear el token Bearer local del Agent en el frontend (navegador), inyectarlo en requests HTTP/WS hacia la API local del Agent (localhost:8384).

Función pública: getAgentLocalToken() -> Promise<string | null>
Ubicación: static/js/modules/agent_auth.js


Arquitectura

Ciclo de obtención del token

  1. Descubrimiento (/info GET)

    • Llamada al Agent sin token: GET http://localhost:8384/info
    • Respuesta incluye metadatos: { agent_id, version, ...}
    • No requiere Bearer (en EXEMPT_PATHS)
  2. Recuperación (GET /info tras login SaaS)

    • User hace login en crearack.com
    • Frontend (login.html) redirige a /info tras iniciar sesión SaaS
    • SaaS incluye en la respuesta el token local que depositó el Agent en handshake inicial
    • Frontend almacena en variable local agentToken
  3. Inyección en requests

    • Toda llamada a /api/* (local) añade header: Authorization: Bearer <token>
    • Middleware local (terminal/middleware.py) valida con verify_local_token()
  4. Cacheo con retries

    • Antes (Agent 2.1.0): getAgentLocalToken() memoizaba el resultado UNA vez. Si primera obtención devolvía null (Agent arrancando sin token depositado), promesa quedaba cached → página condenada a 401s perpetuos.
    • Ahora (Agent 2.1.1): null resetea la promesa → siguiente llamada reintenta → eventual éxito sin F5.

Componentes

fetchToken() -> Promise<string | null>

getAgentLocalToken() -> Promise<string | null>

let tokenPromise = null;

function getAgentLocalToken() {
    // Memoiza el token UNA vez por carga de página, pero NO cachea el fallo:
    // si la 1ª obtención devuelve null (Agent recién arrancado, token aún no
    // depositado, o sin sesión), se resetea la promesa para reintentar en la
    // siguiente llamada.
    if (!tokenPromise) {
        tokenPromise = fetchToken().then((token) => {
            if (!token) tokenPromise = null;  // Reset si falla
            return token;
        });
    }
    return tokenPromise;
}

Lógica:

  1. Primera llamada: tokenPromise = null → ejecuta fetchToken()
  2. Si devuelve token: cachea + devuelve
  3. Si devuelve null: resetea tokenPromise = null para siguiente llamada (reintento)
  4. Siguiente llamada: tokenPromise = null nuevamente → reexecuta fetchToken()

Flujo de uso

En requests HTTP

// ej. GET /api/cluster/list
async function listClusters() {
    const token = await getAgentLocalToken();
    const headers = {};
    
    if (token) {
        headers["Authorization"] = `Bearer ${token}`;
    }
    
    const response = await fetch("http://localhost:8384/api/cluster/list", {
        method: "GET",
        headers,
    });
    
    if (response.status === 401 && token) {
        // Reintentar sin token o con token fresco
        return listClusters();
    }
    
    return response.json();
}

En WebSocket (consola del Agent)

// ej. terminal.js
async function connectTerminal() {
    const token = await getAgentLocalToken();
    
    const ws = new WebSocket("ws://localhost:8384/ws/terminal", {
        // Suele ir en mensaje de handshake, no en header HTTP
    });
    
    ws.onopen = () => {
        ws.send(JSON.stringify({
            type: "connect",
            auth_token: token,  // Enviado en mensaje, no en header
        }));
    };
}

Integración con bootstrap del Agent

Problema (Agent 2.1.0)

  1. .exe arranca, genera token, lo persiste (DPAPI)
  2. Frontend carga, intenta getAgentLocalToken()
  3. SaaS aún no ha depositado el token en su base de datos
  4. fetchToken() → null
  5. getAgentLocalToken() cachea el null → tokenPromise = Promise<null>
  6. Todas las llamadas posteriores usan token null
  7. Middleware local rechaza (sin Bearer válido) → 401s perpetuos
  8. Dashboard queda bloqueado hasta F5

Solución (Agent 2.1.1)

tokenPromise = fetchToken().then((token) => {
    if (!token) tokenPromise = null;  // <<< NUEVO: resetea en fallo
    return token;
});

Ahora:

  1. 1ª llamada devuelve null → cachea fallo
  2. 2ª llamada: tokenPromise fue reseteado → reintenta fetchToken()
  3. Mientras tanto, Agent depositó token en SaaS
  4. fetchToken() ahora devuelve token válido
  5. Token se cachea, rest of requests OK

Variables de control

Variable (local)Propósito
tokenPromiseAlmacena la promesa del token para memoización (con retries en fallos)
agentTokenAlias/almacenamiento alternativo si ya está en sesión

Integración con middleware HTTP

El middleware local valida todo request:

# terminal/middleware.py
def __call__(self, scope, receive, send):
    auth_header = dict(scope.get("headers", [])).get(b"authorization", b"").decode()
    
    if path not in EXEMPT_PATHS:
        if auth_required(auth_manager):
            if not auth_header.startswith("Bearer "):
                return 401_response()
            token = auth_header.split(" ")[1]
            if not verify_local_token(token):
                return 401_response()
    
    return self.app(scope, receive, send)

Cuando getAgentLocalToken() inyecta el Bearer, middleware lo valida contra verify_local_token().


Historias de versión

Agent 2.1.0 (s128)

Agent 2.1.1 (s129 — Fix arranque)


Requisitos de cliente (navegador)


Véase también

Subir