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:
-
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/zohomostrara “sesión actual: desconocida” y Edu no viera el botón “Re-autorizar”. -
JWT
Cf-Access-Jwt-Assertion— siempre presente cuando hay sesión CF Access activa. El claimemailestá 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 haceresolveTeamMemberconsultandoSTAFF_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
| Archivo | Uso |
|---|---|
functions/_lib/staff.ts | resolveTeamMember() la usa internamente para obtener el email antes de hacer el lookup |
functions/api/oauth/zoho/start.ts | Llama 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]]