Skip to main content

Autenticación

Cómo funciona

El panel no emite sesiones propias: delega todo en ajolote (better-auth). El esquema es híbrido:

  • Browser ↔ ajolote: cookie httpOnly first-party emitida por ajolote (hace de refresh token).
  • Browser ↔ backend: el front cambia la cookie por un JWT RS256 corto (GET /api/auth/token) y lo manda como Authorization: Bearer en las llamadas tRPC/REST.
  • Backend: valida el JWT localmente contra el JWKS de ajolote. No guarda sesión ni consulta la DB para autenticar.

El JWT está atado a:

  • issuer = AUTH_SERVICE_URL (backend) — debe ser idéntico a la base URL con la que ajolote firma (BETTER_AUTH_URL).
  • audience = AUTH_APP_ID (backend) — debe coincidir con el appId que pide el front (hoy 'pupo', hardcodeado en auth-client.ts). Mismatch → INVALID_APP_ID.

Diferencia clave entre prod y pre

ProdPre
ajolotepúblico (DigitalOcean)Tailscale-only (http://100.64.188.115:3010)
Cómo llega el front a ajolotedirectovía reverse-proxy del backend
VITE_AUTH_URL (front)URL pública del ajolote de prodel dominio del backend de pre (https://pupo-pre.anfibia.io)
ENABLE_AUTH_PROXY (back)offtrue

En prod, ajolote es público y el browser le pega directo. En pre, ajolote es Tailscale-only: el browser (en internet, fuera de la tailnet) no puede alcanzarlo. El backend sí (está en los dos mundos), así que hace de intermediario.

El reverse-proxy /api/auth (solo pre)

Implementado en src/common/ajolote/auth-proxy.ts (configureAuthProxy), montado en main.ts antes del body-parser (bodyParser: false, luego se restaura json()/urlencoded()). Se activa solo si ENABLE_AUTH_PROXY=true, así prod queda intacto. Qué hace:

  • Proxya /api/auth/* a AUTH_SERVICE_URL (ajolote por Tailscale).
  • Reescribe la cookie a SameSite=None; Secure (front y backend son cross-site).
  • Normaliza el Origin a FRONT_URL para el CSRF (trustedOrigins) de ajolote → así ajolote necesita un solo origin y los preview deployments andan.
  • Maneja el CORS real hacia el browser (credentials: true, refleja el origin; regex opcional AUTH_PROXY_ORIGIN_REGEX para previews).

Ver la guía reusable completa en el repo: auth-panel/docs/AJOLOTE_PROXY.md.

:::note Detalle de implementación cors es CommonJS y el proyecto no usa esModuleInterop, por eso se importa con import cors = require("cors") (el default import compilaba a cors_1.default, undefined en runtime). express se declaró como dependencia directa (venía solo como transitiva y con pnpm no resolvía en el node_modules de producción). :::

Variables de auth

VariableProdPre
AUTH_SERVICE_URL (back)ajolote prod públicohttp://100.64.188.115:3010 (ajolote dev Tailscale)
AUTH_APP_ID (back)pupopupo
ENABLE_AUTH_PROXY (back)true
FRONT_URL (back)https://dev.pupo-panel.pages.dev
CORS_ORIGIN (back)dominio front prodhttps://dev.pupo-panel.pages.dev
AJOLOTE_SERVICE_KEY (back)key del ajolote prodkey del ajolote dev
VITE_AUTH_URL (front)ajolote prod públicohttps://pupo-pre.anfibia.io (el backend)

Del lado de ajolote (dev)

  • BETTER_AUTH_URL debe ser idéntico a AUTH_SERVICE_URL del backend (http://100.64.188.115:3010) — es el iss del JWT.
  • ALLOWED_ORIGINS debe incluir el valor de FRONT_URL (https://dev.pupo-panel.pages.dev). Gracias a la normalización de Origin del proxy, alcanza con ese único origin (no hace falta wildcard).
  • Los usuarios admin deben existir en el ajolote de dev (el login se valida ahí, no en el panel).