CreaRack-SL

resolveActorEmail — Resolución de identidad CF Access vía header + JWT

resolveActorEmail — Resolución de identidad CF Access vía header + JWT

Función exportada en functions/_lib/staff.ts que determina el email del usuario autenticado en una request que pasa por Cloudflare Access. Es la fuente de verdad de identidad para todos los endpoints del workspace que necesitan saber “quién hace esta petición”.

Problema que resuelve

CF Access puede entregar la identidad del usuario autenticado de dos formas:

  1. Header Cf-Access-Authenticated-User-Email — solo disponible si la aplicación CF Access tiene activada la opción “Include identity” en las cookies/headers. En el workspace de CreaRack esta opción estaba desactivada, lo que causaba que el panel /settings/integrations/zoho mostrara “sesión actual: desconocida” y Edu no viera el botón “Re-autorizar”.

  2. JWT Cf-Access-Jwt-Assertion — siempre presente cuando hay sesión CF Access activa. El claim email está incluido en el payload cuando la política tiene identidad de usuario.

Antes del PR#46, resolveTeamMember dependía exclusivamente del header (caso 1). Si no llegaba, devolvía null aunque el usuario estuviera correctamente autenticado.

Implementación

// functions/_lib/staff.ts

function decodeJwtPayload(jwt: string): Record<string, unknown> | null {
  try {
    const parts = jwt.split('.');
    if (parts.length !== 3) return null;
    const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
    const padded = payload + '==='.slice((payload.length + 3) % 4);
    return JSON.parse(atob(padded)) as Record<string, unknown>;
  } catch {
    return null;
  }
}

export function resolveActorEmail(request: Request): string | null {
  // 1. Intenta el header standard (requiere "Include identity" activo en CF Access)
  const headerEmail =
    request.headers.get('cf-access-authenticated-user-email')?.toLowerCase() ?? null;
  if (headerEmail) return headerEmail;

  // 2. Fallback: decode del JWT (siempre disponible con sesión CF Access)
  const jwt = request.headers.get('cf-access-jwt-assertion');
  if (!jwt) return null;
  const payload = decodeJwtPayload(jwt);
  const email = payload && typeof payload.email === 'string' ? payload.email : null;
  return email ? email.toLowerCase() : null;
}

Seguridad del JWT decode sin verificar firma

El decode se hace sin validar la firma (no se comprueba contra las claves públicas de CF Access). Esto es seguro en este contexto porque:

  • El middleware de CF Access (_middleware.ts) ya ha validado el JWT antes de que el handler sea invocado. Solo las requests con JWT válido llegan a los handlers.
  • CF Access es el emisor del JWT y solo lo entrega a sesiones autenticadas. No hay vector de ataque externo que pueda inyectar un JWT falso que el middleware no rechace.
  • El decode sirve únicamente para leer el claim email — no se usa para tomar decisiones de autorización adicionales (eso lo hace resolveTeamMember consultando STAFF_EMAIL_TO_NAME).

⚠️ Advertencia: si en algún momento se expone un endpoint a requests no filtradas por CF Access middleware, este decode NO debe usarse como prueba de identidad.

Cadena de resolución completa

Request
  └─ resolveActorEmail(request)
       ├─ header cf-access-authenticated-user-email → email (si existe)
       └─ header cf-access-jwt-assertion → decodeJwtPayload → payload.email
            └─ resolveTeamMember(request)
                 └─ STAFF_EMAIL_TO_NAME[email] → TeamMember | null

Consumidores actuales

ArchivoUso
functions/_lib/staff.tsresolveTeamMember() la usa internamente para obtener el email antes de hacer el lookup
functions/api/oauth/zoho/start.tsLlama directamente a resolveActorEmail() para incluir el email en el campo created_by del state OAuth (auditoría)

Tabla de staff mapeado

const STAFF_EMAIL_TO_NAME: Record<string, TeamMember> = {
  'edu@crearack.com':     'Edu',
  'dani@edomo.net':       'Dani',
  'tfuentes@edomo.net':   'Txell',
};

El tipo TeamMember = 'Edu' | 'Dani' | 'Txell' es exhaustivo — cualquier email no listado devuelve null y resulta en 403.

Véase también

  • [[feature—workspace—zoho-oauth-admin-mode]]
  • [[feature—workspace—zoho-oauth-flow]]
  • [[entity—workspace—table—zoho-oauth-state]]