Jamdesk Documentation logo

Autenticación JWT

Protege tus docs con tu propio sistema de acceso. Activa la autenticación JWT en docs.json y firma tokens de corta duración para sesiones por usuario.

La autenticación JWT requiere un plan de pago y un proyecto Jamdesk conectado a un repositorio Git. La configuración vive en docs.json, así que viaja junto con tu flujo habitual de build y despliegue.

Si tu producto ya tiene su propio sistema de acceso, la autenticación JWT te permite proteger tus docs con él en lugar de repartir una contraseña compartida. Tu backend firma un token de corta duración cuando un usuario con sesión iniciada accede a los docs. Jamdesk lo verifica una vez, crea una sesión, y el visitante navega con normalidad a partir de entonces. Los visitantes nunca necesitan una cuenta de Jamdesk ni una contraseña compartida.

En qué se diferencia de la protección por contraseña

Protección por contraseña da a cada visitante la misma frase de acceso compartida, lo cual funciona bien para docs internos, previews de staging, o una única audiencia de partner. La autenticación JWT es por usuario: la identidad, la duración de sesión y el acceso a páginas de cada visitante provienen de un token que firma tu backend. El acceso a los docs puede seguir tus cuentas de cliente, planes o roles existentes en lugar de un único secreto compartido.

Los dos modos son mutuamente excluyentes: auth.password y auth.jwt no pueden estar activados a la vez. Consulta Migrar desde la protección por contraseña más abajo si estás cambiando de uno a otro.

Pasos de configuración

1
Activar auth.jwt en docs.json
docs.json
{
  "$schema": "https://jamdesk.com/docs.json",
  "name": "Acme Docs",
  "theme": "jam",
  "auth": {
    "jwt": {
      "enabled": true,
      "loginUrl": "https://app.example.com/docs-login",
      "public": ["/changelog/*"]
    }
  }
}

loginUrl es obligatorio siempre que enabled: true, y debe ser una URL absoluta en https://. Los visitantes no autenticados son redirigidos aquí con ?redirect=<path> para que tu flujo de acceso sepa a dónde reenviarlos. public es opcional: rutas o globs (* para un segmento, ** para cualquier profundidad) que permanecen accesibles sin iniciar sesión.

2
Generar la clave de firma

Abre Project Settings en el dashboard y busca la tarjeta JWT authentication. Haz clic en Generate signing key.

Jamdesk crea un par de claves Ed25519, conserva solo la clave pública, y te muestra la clave privada exactamente una vez. Cópiala de inmediato en tu gestor de secretos. Jamdesk nunca almacena ni envía por correo la clave privada, y no puede recuperarla si la pierdes. Si eso ocurre, rota la clave. La rotación es un cambio definitivo, así que lee Rotación de la clave de firma antes de hacer clic.

3
Commit y rebuild
git add docs.json
git commit -m "Turn on JWT authentication"
git push

Una vez que el build se publica, el sitio protege cada página. Las solicitudes sin una sesión válida se redirigen a tu loginUrl.

Integra tu flujo de acceso

Cuando un usuario con sesión iniciada entra a tus docs, tu backend firma un JWT y redirige el navegador a la URL de callback del sitio de docs con el token en el fragmento de la URL (después del #). Los fragmentos nunca llegan a tus logs de servidor ni a ningún reverse proxy, porque los navegadores no los envían con la solicitud.

El token debe estar firmado con EdDSA (Ed25519, coincidiendo con la clave que generaste en el dashboard), y su claim exp no debería superar unos 10 segundos en el futuro. Eso es una ventana de handshake, no una duración de sesión. La duración de sesión real se controla por separado mediante el campo expiresAt en el payload (consulta la referencia del payload más abajo).

import { SignJWT, importPKCS8 } from "jose";

// Store this in your secret manager. It's the private key Jamdesk showed
// you once when you generated it in Project Settings.
const privateKey = await importPKCS8(process.env.JAMDESK_JWT_PRIVATE_KEY!, "EdDSA");

async function signDocsToken(user: { groups: string[]; apiToken: string }) {
  return new SignJWT({
    host: "acme.jamdesk.app", // or your custom domain, e.g. "docs.example.com"
    expiresAt: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, // 7-day session
    groups: user.groups,
    apiPlaygroundInputs: {
      header: { Authorization: `Bearer ${user.apiToken}` },
    },
  })
    .setProtectedHeader({ alg: "EdDSA" })
    .setExpirationTime("10s") // handshake window, not session length
    .sign(privateKey);
}

// In your "open docs" route/button handler:
app.get("/docs-login", requireAuth, async (req, res) => {
  const token = await signDocsToken(req.user);
  const redirect = req.query.redirect ?? "/";
  res.redirect(
    `https://acme.jamdesk.app/_jd/auth/callback?redirect=${encodeURIComponent(
      String(redirect)
    )}#${token}`
  );
});

Firma el token solo del lado del servidor. La clave privada nunca debe llegar a un navegador ni a un repo público. Cualquiera que la tenga puede crear sesiones para tu sitio de docs.

Flujo de redirección

  1. Un visitante solicita una página protegida (digamos, /quickstart) sin una sesión válida. Jamdesk responde con una redirección a {loginUrl}?redirect=%2Fquickstart.
  2. Tu flujo de acceso autentica al visitante (como lo hagas normalmente), firma un JWT, y lo redirige a https://<your-docs-host>/_jd/auth/callback?redirect=%2Fquickstart#<jwt>.
  3. La página de callback lee el token del fragmento del lado del cliente y lo envía al endpoint de intercambio de tokens de Jamdesk. Jamdesk verifica la firma y los claims, y si tiene éxito, establece una cookie de sesión firmada.
  4. El navegador es redirigido al destino original, /quickstart, ahora con una sesión válida. El valor redirect se conserva de principio a fin para que los visitantes lleguen exactamente a donde empezaron.

Si tu backend no puede determinar un valor de redirect (alguien guardó tu página de acceso directamente en favoritos, por ejemplo), omítelo y Jamdesk recurre a /.

Páginas públicas

Algunas páginas deben seguir siendo accesibles sin iniciar sesión, como una página de estado o un changelog público. Hay tres formas de marcar una página como pública, y todas se combinan en una sola lista de permitidos:

Frontmatter, para una página a la vez:

---
title: Changelog
public: true
---

Grupos de navegación, para una sección completa:

docs.json
{
  "navigation": {
    "groups": [
      { "group": "Changelog", "public": true, "pages": ["changelog"] }
    ]
  }
}

Globs explícitos, bajo auth.jwt.public[]:

docs.json
{
  "auth": {
    "jwt": {
      "enabled": true,
      "loginUrl": "https://app.example.com/docs-login",
      "public": ["/changelog/*", "/status"]
    }
  }
}

Marcar una página como pública abre la página en sí. Las imágenes y videos que contiene se sirven desde las rutas de assets de tu proyecto, que permanecen detrás de la protección, así que un visitante sin sesión ve una página pública sin sus imágenes. Añade las rutas de assets a auth.jwt.public[] cuando una página pública las necesite:

docs.json
{
  "auth": {
    "jwt": {
      "public": ["/changelog/*", "/status", "/_jd/images/changelog/**"]
    }
  }
}

Limita el glob a las carpetas que realmente usan tus páginas públicas. Las rutas de assets nunca se verifican por grupo, así que un glob amplio como /_jd/images/** sirve todas las imágenes del sitio a cualquiera, incluidas las capturas de pantalla dentro de páginas que restringiste con groups. Mantén las imágenes de las páginas públicas en su propia carpeta y abre solo esa carpeta.

Acceso basado en grupos

Algunas páginas solo deben ser visibles para ciertos usuarios autenticados, como un runbook de administración o una referencia exclusiva para enterprise. Añade groups al frontmatter de una página:

---
title: Admin API Keys
groups: ["admin"]
---

La sesión de un visitante lleva el arreglo groups que tu backend puso en el payload del JWT. Si una página declara groups y la sesión del visitante no se cruza con esa lista, recibe un 404 en lugar de un 401 o una pantalla de desbloqueo. Esto es deliberado: una página restringida por grupo no revela su propia existencia a los usuarios fuera del grupo.

Detalles que afectan cómo usas groups:

  • Las páginas de grupo se excluyen del sitemap, la búsqueda, el chat de IA y el MCP, incluso para usuarios que están en el grupo. La exclusión de estas superficies de descubrimiento es una decisión tomada en tiempo de build, no por visitante. Un miembro del grupo admin puede seguir abriendo /admin/api-keys directamente (por URL o enlace interno), pero no aparecerá en resultados de búsqueda, respuestas del chat, o llms.txt. Si necesitas que una página restringida sea encontrable por su propia audiencia, enlázala desde otra página a la que esa audiencia ya pueda llegar.
  • Un groups: [] vacío significa que no hay ninguna restricción, no «nadie puede ver esto». Para eliminar la restricción de grupo de una página, borra el campo groups por completo en lugar de dejarlo como un arreglo vacío.
  • Para restringir una página a nadie, despublícala. No existe ningún valor de groups que signifique «nadie»: la pertenencia a grupos es aditiva, y cualquier coincidencia otorga acceso.
  • Las copias localizadas heredan automáticamente el groups de la página base, a menos que la traducción declare su propio groups en su frontmatter. Traducir una página restringida no hace que la traducción se vuelva pública por accidente.
  • Jamdesk determina qué carpetas de nivel superior son traducciones a partir de navigation.languages, además de cualquier carpeta de nivel superior nombrada según un código de idioma (fr, it, cs, etc.) que tenga páginas dentro. Una carpeta que solo comparte nombre con un código de idioma, por ejemplo una carpeta it con runbooks de TI, también se trata como una traducción, y sus páginas heredan el groups de cualquier página raíz en la misma ruta. Esto solo puede restringir más, nunca menos. Renombra la carpeta si te estorba.
  • groups restringe páginas, no las imágenes, videos y otros archivos que una página incrusta. Un asset al que solo enlaza una página restringida se sigue sirviendo a cualquier visitante con sesión iniciada que solicite su URL, sin importar qué grupos lleve su sesión. Las URLs de los assets siguen las rutas de archivo de tu repo, así que un nombre como images/admin/sso-config.png es fácil de adivinar. Mantén fuera del repo de docs cualquier cosa que no quieras que vea cualquier lector con sesión iniciada.
  • La barra lateral, los tabs, las breadcrumbs y los enlaces anterior/siguiente se filtran por visitante. Una página que los grupos del visitante no cubren se omite, y un grupo o tab que termina vacío se omite junto con ella, así que el nombre de una sección restringida no se muestra a las personas fuera de ella. Este filtrado ocurre en el momento de la solicitud y es independiente de las exclusiones en tiempo de build mencionadas arriba, que aplican para todos.
  • Mantén los nombres de grupo cortos. Los grupos viajan dentro de la cookie de sesión: hasta 32 grupos por sesión, 64 caracteres cada uno. Superar cualquiera de los dos límites no recorta la lista; Jamdesk rechaza el token completo con un 401 y no otorga sesión.

Pre-relleno del playground API

Si tus docs tienen un playground API, puedes pre-rellenarlo para visitantes con sesión iniciada para que no tengan que pegar su propia clave de API. Incluye apiPlaygroundInputs en el payload de tu JWT:

{
  "host": "acme.jamdesk.app",
  "apiPlaygroundInputs": {
    "header": { "Authorization": "Bearer sk_live_user_specific_token" },
    "query": { "org_id": "acme-corp" },
    "path": { "workspace_id": "ws_123" }
  }
}
  • header.Authorization pre-rellena el campo de autenticación del playground. Un prefijo Bearer se elimina automáticamente si está presente.
  • query y path pre-rellenan cualquier nombre de parámetro coincidente en el endpoint actual.
  • Las secciones server y cookie no son compatibles. Solo se aplican header, query, y path.
  • El pre-relleno nunca sobrescribe un valor que el visitante ya haya escrito en el playground.

Referencia del payload

CampoObligatorioDescripción
hostSíDebe coincidir exactamente con el host de la solicitud (sin distinguir mayúsculas/minúsculas): tu subdominio *.jamdesk.app o tu dominio personalizado. Un token firmado para un host es rechazado en cualquier otro.
expiresAtNoMarca de tiempo Unix (en segundos) de cuándo debe expirar la sesión resultante. Con un tope de 30 días; por defecto 7 días si se omite. Esto es independiente de la claim exp propia del token, de corta duración.
groupsNoArreglo de nombres de grupo que la sesión debe llevar, hasta 32 entradas de 64 caracteres cada una. Superar cualquiera de los dos límites rechaza el token completo (401, sin sesión) en lugar de truncar la lista.
apiPlaygroundInputsNoValores de pre-relleno para el playground API. El tamaño serializado tiene un tope de 2 KB. Si no cabe, se descarta sin error y la sesión de todas formas se otorga.

Rotación de la clave de firma

Rotate signing key en la tarjeta del dashboard genera un nuevo par de claves y muestra la nueva clave privada una vez, de la misma forma que la primera generación. No hay periodo de solapamiento. En unos 15 segundos, la clave anterior deja de ser aceptada y todas las sesiones existentes terminan. Hasta que tu backend firme con la nueva clave, cada inicio de sesión es rechazado y los visitantes rebotan entre tu página de acceso y los docs.

Por eso el orden importa:

  1. Ten listo un deploy que lea la clave de firma desde tu gestor de secretos en lugar de un valor fijo en el código.
  2. Haz clic en Rotate signing key y copia la nueva clave privada.
  3. Actualiza el secreto y despliega. Los inicios de sesión vuelven a funcionar en cuanto tu backend use la nueva clave.

Rota la clave en un momento tranquilo si puedes, y avisa a quien sea responsable del deploy del backend antes de hacer clic.

Si solo quieres terminar la sesión de todos, por ejemplo después de perder un portátil, usa Revoke sessions en su lugar. Esto conserva la clave, así que nada cambia en tu backend; cada visitante solo tiene que iniciar sesión de nuevo.

Clear signing key elimina la clave pública de Jamdesk. auth.jwt sigue activado en docs.json, así que el sitio permanece protegido, pero no se puede verificar ningún token hasta que generes una nueva clave. Elimínala solo cuando estés moviendo el sitio a otro modo de acceso o retirándolo.

Cierre de sesión

Los visitantes con sesión iniciada obtienen un enlace Log out en el encabezado de los docs. Los envía a /_jd/auth/logout, que borra la cookie de sesión y redirige a tu loginUrl. También puedes enlazarlo directamente desde tu propia aplicación si quieres un enlace de «cerrar sesión de los docs» en otro lugar. Es una simple solicitud GET sin cuerpo ni headers requeridos.

Cerrar sesión en los docs no cierra la sesión del visitante en tu producto. Si tu flujo de acceso firma un token para cualquiera que ya tenga una sesión en la app, un visitante que hace clic en Log out y luego abre un enlace a los docs vuelve a iniciar sesión de inmediato. Esto suele ser lo que quieres. Si necesitas un cierre de sesión real, haz que tu manejador de loginUrl verifique un inicio de sesión explícito en lugar de crear un token automáticamente, o haz que tu propio cierre de sesión también apunte a la URL de cierre de sesión de los docs.

Comportamiento de funciones bajo autenticación

FunciónComportamiento
llms.txt / llms-full.txt / sitemapProtegido junto con el resto del sitio: inalcanzable sin una sesión válida, igual que cualquier otra página.
Páginas restringidas por grupoExcluidas de todos los artefactos anteriores, además de la búsqueda y el chat de IA, sin importar los grupos de la sesión solicitante (ver Acceso basado en grupos).
robots.txtSiempre público. Los motores de búsqueda pueden ver que existe un sitio de docs y que está protegido; no pueden ver su contenido.

Solución de problemas

La rotación y la revocación tardan unos 15 segundos en surtir efecto, no al instante, porque la puerta de enlace en el edge almacena brevemente en caché la configuración de autenticación para mantener rápida cada solicitud de página. Rotate en el dashboard sí invalida todas las sesiones existentes; espera hasta 15 segundos antes de considerar como un bug una sesión antigua que aún parece válida.

El dashboard y la caché del runtime no están de acuerdo sobre tu clave de firma, generalmente porque un fallo de escritura temporal interrumpió una generación, rotación, o eliminación. El banner indica en qué sentido: o la clave más reciente aún no ha llegado a la caché (los tokens firmados con ella pueden ser rechazados), o una clave que eliminaste todavía está en caché (los tokens firmados con ella todavía se aceptan). Jamdesk vuelve a verificar cada vez que abres la página de configuración. Si el banner persiste, haz clic en Retry sync. Si eso sigue fallando, rota la clave, o genera y elimina de nuevo en el caso de la clave eliminada. Un banner que dice que Jamdesk no pudo verificar el estado en absoluto significa que la propia verificación falló; reinténtalo cuando el runtime sea accesible.

Verifica la claim host contra el host exacto que se está solicitando. Si tus docs son accesibles tanto en un dominio personalizado (docs.example.com) como en el subdominio *.jamdesk.app subyacente, un token firmado para uno será rechazado en el otro: la vinculación de host es exacta e insensible a mayúsculas/minúsculas, pero no reconoce alias. Firma tokens para el host que realmente uses, o firma dos variantes si enlazas a ambos.

La ruta de callback de Jamdesk se niega a redirigir de vuelta hacia sí misma: un valor redirect que apunta a /_jd/auth/callback (o la página estilo desbloqueo debajo de ella) se reescribe a / en lugar de respetarse. Si sigues viendo un bucle, verifica que tu flujo de acceso no esté redirigiendo él mismo al loginUrl de los docs en un ciclo (por ejemplo, una página de acceso que rebota de inmediato a /docs-login cuando no encuentra una sesión de docs). El lado de los docs del bucle está protegido; el bucle casi siempre está en el flujo de acceso.

Esto es un config_error y bloquea el build. Elige uno; consulta Migrar desde la protección por contraseña para el orden seguro de operaciones si estás cambiando.

Nota de seguridad

apiPlaygroundInputs, incluido cualquier valor de Authorization que pongas en él, es legible por el JavaScript que se ejecuta en tu sitio de docs a través del endpoint session-info que alimenta el pre-relleno del playground. El pre-relleno es conveniente, pero no es un lugar para secretos de alto privilegio.

Envía credenciales por usuario, de mínimo privilegio, limitadas a lo que ese visitante tiene permitido hacer, nunca una clave de administrador válida para toda la organización. Trata cualquier cosa que pongas en apiPlaygroundInputs como visible para la persona que navega los docs, porque lo es.

Migrar desde la protección por contraseña

Cambiar de una contraseña compartida a la autenticación JWT no requiere tiempo de inactividad, y el sitio permanece protegido durante todo el proceso. Hazlo en este orden:

1
Generar la clave de firma JWT

Haz esto primero, mientras la protección por contraseña todavía está activa. Generar una clave no cambia lo que está protegido; la contraseña permanece vigente todo el tiempo.

2
Cambiar docs.json y rebuild
docs.json
{
  "auth": {
    "password": { "enabled": false },
    "jwt": { "enabled": true, "loginUrl": "https://app.example.com/docs-login" }
  }
}

Haz commit y push. En el momento en que se publica este build, la protección cambia atómicamente de contraseña a JWT, sin ninguna ventana en la que el sitio quede desprotegido. Cualquier sesión desbloqueada por contraseña que existiera termina en ese cambio; los visitantes se autentican a través de tu flujo de acceso a partir de entonces.

3
Eliminar la contraseña

Una vez que hayas confirmado que el flujo JWT funciona de principio a fin, vuelve a Project Settings y elimina la contraseña almacenada. En este punto es inerte (el modo contraseña está desactivado en docs.json), pero eliminarla borra por completo el hash almacenado.

¿Qué sigue?

Resumen del control de acceso

Compara la autenticación JWT con la protección por contraseña, el SSO, y el patrón multi-proyecto.

Protección por contraseña

La alternativa de frase de acceso compartida: más simple de configurar, sin necesidad de integración con el backend.

SSO (Enterprise)

Inicio de sesión gestionado por un proveedor de identidad para clientes enterprise.

Dominios personalizados

Pon tus docs en tu propio dominio antes de configurar tu flujo de acceso.