---
title: Autenticação JWT
description: >-
  Proteja sua documentação com seu próprio sistema de login. Ative a autenticação JWT no
  docs.json e assine tokens curtos para sessões individuais.
---

> **For AI agents:** the complete documentation index is at [llms.txt](https://jamdesk.com/docs/llms.txt). Append `.md` to any page URL for its markdown version.

<Note>
  A autenticação JWT exige um plano pago e um projeto Jamdesk [conectado a um repositório Git](https://jamdesk.com/docs/pt/setup/connecting-github). A configuração fica no `docs.json`, acompanhando seu fluxo normal de build e deploy.
</Note>

Se o seu produto já tiver um sistema de login próprio, a autenticação JWT permite protegê-lo em vez de distribuir uma senha compartilhada. Seu backend assina um token de curta duração quando um usuário autenticado acessa a documentação. O Jamdesk o verifica uma vez, cria uma sessão, e o visitante navega normalmente a partir daí. Os visitantes nunca precisam de uma conta Jamdesk ou de uma senha compartilhada.

## Como isso difere da proteção por senha

A [proteção por senha](https://jamdesk.com/docs/pt/setup/password-protection) fornece a mesma senha compartilhada a todos os visitantes, o que funciona bem para documentação interna, prévias de staging ou um único público de parceiros. A autenticação JWT é por usuário: a identidade, a duração da sessão e o acesso às páginas de cada visitante vêm de um token assinado pelo *seu* backend. O acesso à documentação pode seguir suas contas de clientes, planos ou funções existentes, em vez de um único segredo compartilhado.

Os dois modos são mutuamente exclusivos: `auth.password` e `auth.jwt` não podem estar ativados ao mesmo tempo. Consulte [Migrar da proteção por senha](#migrar-da-proteção-por-senha) abaixo se estiver mudando de um para o outro.

## Etapas de configuração

### 1. Ative auth.jwt no docs.json

```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` é obrigatório sempre que `enabled: true` e deve ser uma URL absoluta `https://`. Visitantes não autenticados são redirecionados para esse endereço com `?redirect=<path>`, para que seu fluxo de login saiba para onde enviá-los de volta. `public` é opcional: caminhos ou globs (`*` para um segmento, `**` para qualquer profundidade) que continuam acessíveis sem autenticação.

### 2. Gere a chave de assinatura

Abra **Project Settings** no dashboard e encontre o card **JWT authentication**. Clique em **Generate signing key**.

O Jamdesk cria um par de chaves Ed25519, mantém apenas a chave *pública* e mostra a chave *privada* uma única vez. Copie-a imediatamente para seu gerenciador de segredos. O Jamdesk nunca armazena nem envia a chave privada por e-mail, e não pode recuperá-la se você a perder. Nesse caso, faça a rotação da chave. A rotação é uma troca imediata; leia [Rotacionar a chave de assinatura](#rotação-da-chave-de-assinatura) antes de clicar.

### 3. Faça commit e rebuild

```bash
git add docs.json
git commit -m "Turn on JWT authentication"
git push
```

Assim que o build for publicado, o site protegerá todas as páginas. As solicitações sem uma sessão válida serão redirecionadas para seu `loginUrl`.

## Integre seu fluxo de login

Quando um usuário autenticado acessa sua documentação, seu backend assina um JWT e redireciona o navegador para a URL de callback do site de documentação, com o token no fragmento da URL (após o `#`). Fragmentos nunca chegam aos logs do servidor nem a nenhum proxy reverso, porque os navegadores não os enviam com a solicitação.

O token deve ser assinado com EdDSA (Ed25519, correspondente à chave gerada no dashboard), e sua claim `exp` deve estar no máximo cerca de 10 segundos no futuro. Essa é uma janela de handshake, não a duração da sessão. A duração real da sessão é controlada separadamente pelo campo `expiresAt` no payload (consulte a [referência do payload](#referência-do-payload) abaixo).

<CodeGroup>
```typescript TypeScript (jose)
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}`
  );
});
```

```python Python (pyjwt)
import time
from urllib.parse import quote

import jwt  # PyJWT >= 2.4, with the cryptography extra installed
from flask import redirect, request  # or your framework's equivalents

with open("jamdesk_jwt_private_key.pem", "rb") as f:
    PRIVATE_KEY = f.read()

def sign_docs_token(user):
    payload = {
        "host": "acme.jamdesk.app",  # or your custom domain
        "exp": int(time.time()) + 10,  # handshake window, not session length
        "expiresAt": int(time.time()) + 60 * 60 * 24 * 7,  # 7-day session
        "groups": user.groups,
        "apiPlaygroundInputs": {
            "header": {"Authorization": f"Bearer {user.api_token}"},
        },
    }
    return jwt.encode(payload, PRIVATE_KEY, algorithm="EdDSA")

@app.route("/docs-login")
def docs_login():
    token = sign_docs_token(current_user)
    redirect_path = request.args.get("redirect", "/")
    return redirect(
        f"https://acme.jamdesk.app/_jd/auth/callback"
        f"?redirect={quote(redirect_path)}#{token}"
    )
```
</CodeGroup>

<Warning>
  Assine o token somente no servidor. A chave privada nunca deve chegar a um navegador ou repositório público. Qualquer pessoa que a possua pode criar sessões para seu site de documentação.
</Warning>

## Fluxo de redirecionamento

1. Um visitante solicita uma página protegida (por exemplo, `/quickstart`) sem uma sessão válida. O Jamdesk responde com um redirecionamento para `{loginUrl}?redirect=%2Fquickstart`.
2. Seu fluxo de login autentica o visitante (da forma como você normalmente faz), assina um JWT e o redireciona para `https://<your-docs-host>/_jd/auth/callback?redirect=%2Fquickstart#<jwt>`.
3. A página de callback lê o token do fragmento no cliente e o envia ao endpoint de troca de tokens do Jamdesk. O Jamdesk verifica a assinatura e as claims e, em caso de sucesso, define um cookie de sessão assinado.
4. O navegador é redirecionado ao destino original, `/quickstart`, agora com uma sessão válida. O valor de `redirect` é preservado de ponta a ponta, para que os visitantes cheguem exatamente ao local de onde partiram.

Se seu backend não puder determinar um valor de `redirect` (por exemplo, alguém marcou sua página de login diretamente), omita-o e o Jamdesk usará `/`.

## Páginas públicas

Algumas páginas devem continuar acessíveis sem autenticação, como uma página de status ou um changelog público. Há três maneiras de marcar uma página como pública, e todas são combinadas em uma única lista de permissões:

**Frontmatter**, uma página por vez:

```yaml
---
title: Changelog
public: true
---
```

**Grupos de navegação**, para uma seção inteira:

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

**Globs explícitos**, em `auth.jwt.public[]`:

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

Marcar uma página como pública abre a página em si. As imagens e os vídeos nela são servidos a partir dos caminhos de recursos do seu projeto, que continuam atrás da proteção, portanto um visitante sem sessão vê uma página pública sem as ilustrações. Adicione esses caminhos a `auth.jwt.public[]` quando uma página pública precisar deles:

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

Limite o glob às pastas que as suas páginas públicas realmente usam. Os caminhos de recursos nunca são verificados em relação aos grupos, portanto um glob amplo como `/_jd/images/**` serve todas as imagens do site a qualquer pessoa, incluindo as capturas de tela das páginas que você restringiu com `groups`. Mantenha as imagens das páginas públicas numa pasta própria e abra apenas essa pasta.

## Acesso baseado em grupos

Algumas páginas devem ser visíveis apenas para determinados usuários autenticados, como um runbook de administração ou uma referência exclusiva para empresas. Adicione `groups` ao frontmatter da página:

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

A sessão do visitante contém o array `groups` que seu backend colocou no payload do JWT. Se uma página declarar `groups` e a sessão do visitante não tiver interseção com essa lista, ele receberá um 404 em vez de um 401 ou de uma tela de desbloqueio. Isso é intencional: uma página restrita a um grupo não revela sua própria existência a usuários de fora do grupo.

Detalhes que afetam o uso de `groups`:

- Páginas de grupo são excluídas do sitemap, da pesquisa, do chat de IA e do MCP, mesmo para usuários que pertencem ao grupo. A exclusão dessas superfícies de descoberta é uma decisão tomada no build, não por visitante. Um membro do grupo `admin` ainda pode abrir `/admin/api-keys` diretamente (por URL ou link interno), mas a página não aparecerá nos resultados de pesquisa, nas respostas do chat ou no `llms.txt`. Se precisar que uma página restrita seja encontrada pelo próprio público, crie um link para ela em outra página que esse público já possa acessar.
- Um `groups: []` vazio significa que não há restrição alguma, não que “ninguém pode ver”. Para remover a restrição de grupo de uma página, exclua o campo `groups` por completo em vez de defini-lo como um array vazio.
- Para restringir uma página a ninguém, cancele sua publicação. Não há um valor de `groups` que signifique “ninguém”: a associação a grupos é aditiva, e qualquer interseção concede acesso.
- Cópias localizadas herdam automaticamente os `groups` da página-base, a menos que a tradução declare seus próprios `groups` no frontmatter. Traduzir uma página restrita não torna acidentalmente a tradução pública.
- O Jamdesk identifica quais pastas de nível superior são traduções a partir de `navigation.languages`, além de qualquer pasta de nível superior nomeada com um código de idioma (`fr`, `it`, `cs` e assim por diante) que contenha páginas. Uma pasta que apenas compartilhe o nome de um código de idioma, como uma pasta `it` com runbooks de TI, também é tratada como tradução, e suas páginas herdam os `groups` de qualquer página-raiz no mesmo caminho. Isso só pode restringir mais, nunca menos. Renomeie a pasta se isso causar problemas.
- `groups` restringe páginas, não as imagens, os vídeos e os demais arquivos que uma página incorpora. Um recurso ao qual apenas uma página restrita faz referência continua sendo servido a qualquer visitante com sessão iniciada que peça sua URL, quaisquer que sejam os grupos da sessão. As URLs de recursos seguem os caminhos de arquivo do seu repositório, então um nome como `images/admin/sso-config.png` é fácil de adivinhar. Mantenha fora do repositório de documentação tudo o que você não quer mostrar a todos os seus leitores com sessão iniciada.
- A barra lateral, as abas, as trilhas de navegação e os links anterior/próximo são filtrados por visitante. Uma página que os grupos do visitante não abrangem é removida, assim como um grupo ou aba que fique vazio; desse modo, o nome de uma seção restrita não é mostrado a pessoas de fora dela. Essa filtragem ocorre no momento da solicitação e é separada das exclusões no build mencionadas acima, que se aplicam a todos.
- Mantenha os nomes dos grupos curtos. Os grupos viajam dentro do cookie de sessão: até 32 grupos por sessão, com 64 caracteres cada. Exceder qualquer limite não reduz a lista; o Jamdesk rejeita o token inteiro com um 401 e não concede nenhuma sessão.

## Preenchimento antecipado do API playground

Se sua documentação tiver um [API playground](https://jamdesk.com/docs/pt/api-reference/playground), você poderá preenchê-lo antecipadamente para visitantes autenticados, para que eles não precisem colar sua própria chave de API. Inclua `apiPlaygroundInputs` no payload do JWT:

```json
{
  "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` preenche antecipadamente o campo de autenticação do playground. Um prefixo `Bearer ` é removido automaticamente quando presente.
- `query` e `path` preenchem antecipadamente todos os nomes de parâmetros correspondentes no endpoint atual.
- As seções `server` e `cookie` não são compatíveis. Somente `header`, `query` e `path` são aplicados.
- O preenchimento antecipado nunca substitui um valor que o visitante já tenha digitado no playground.

## Referência do payload

| Campo | Obrigatório | Descrição |
|---|---|---|
| `host` | Sim | Deve corresponder exatamente ao host da solicitação (sem diferenciar maiúsculas de minúsculas): seu subdomínio `*.jamdesk.app` ou seu domínio personalizado. Um token assinado para um host é rejeitado em qualquer outro. |
| `expiresAt` | Não | Timestamp Unix (em segundos) que define até quando a *sessão* resultante deve permanecer válida. Limitado a 30 dias; o padrão é 7 dias quando omitido. Isso é independente da claim `exp` de curta duração do próprio token. |
| `groups` | Não | Array de nomes de grupos que a sessão deve carregar, com até 32 entradas de 64 caracteres cada. Exceder qualquer limite rejeita o token inteiro (401, sem sessão), em vez de truncar a lista. |
| `apiPlaygroundInputs` | Não | Valores para preenchimento antecipado do API playground. O tamanho serializado é limitado a 2 KB. Se não couber, o campo é descartado sem erro e a sessão ainda é concedida. |

## Rotação da chave de assinatura

**Rotate signing key** no card do dashboard gera um novo par de chaves e mostra a nova chave privada uma única vez, da mesma forma que na primeira geração. Não há período de sobreposição. Em cerca de 15 segundos, a chave antiga deixa de ser aceita e todas as sessões existentes terminam. Até que seu backend assine com a nova chave, todos os logins serão rejeitados e os visitantes ficarão alternando entre sua página de login e a documentação.

Portanto, a ordem é importante:

1. Tenha um deploy pronto que leia a chave de assinatura do seu gerenciador de segredos, em vez de um valor codificado diretamente.
2. Clique em **Rotate signing key** e copie a nova chave privada.
3. Atualize o segredo e faça o deploy. Os logins voltarão a funcionar assim que seu backend estiver usando a nova chave.

Faça a rotação em um horário tranquilo, se possível, e avise quem é responsável pelo deploy do backend antes de clicar.

Se você só quiser encerrar a sessão de todos, por exemplo, depois que um laptop desaparecer, use **Revoke sessions**. Isso mantém a chave, portanto nada muda no seu backend; cada visitante só precisará fazer login novamente.

**Clear signing key** remove a chave pública do Jamdesk. `auth.jwt` continua ativado no `docs.json`, então o site fica bloqueado: os visitantes veem uma página informando que o site é privado em vez da sua página de login, e nenhum token funciona até que você gere uma nova chave. O mesmo acontece se você ativar `auth.jwt` antes de gerar uma chave. As chaves de API do projeto continuam funcionando para a API de pesquisa de docs e o MCP. Limpe-a somente quando estiver migrando o site para outro modo de acesso ou desativando-o.

## Logout

Visitantes autenticados veem um link **Log out** no cabeçalho da documentação. Ele os envia para `/_jd/auth/logout`, que limpa o cookie de sessão e redireciona para seu `loginUrl`. Você também pode criar um link direto para esse endereço no seu próprio app se quiser disponibilizar um link de “sair da documentação” em outro local. É uma solicitação `GET` simples, sem corpo ou cabeçalhos obrigatórios.

Sair da documentação não desconecta o visitante do seu produto. Se seu fluxo de login assinar um token para qualquer pessoa que já tenha uma sessão no app, um visitante que clicar em **Log out** e depois abrir um link da documentação entrará novamente de forma automática. Geralmente é isso que você quer. Se precisar de um logout real, faça o handler de `loginUrl` verificar um login explícito em vez de criar um token automaticamente, ou aponte também o logout do seu app para a URL de logout da documentação.

## Comportamento dos recursos com autenticação

| Recurso | Comportamento |
|---|---|
| `llms.txt` / `llms-full.txt` / sitemap | Protegidos junto com o restante do site: inacessíveis sem uma sessão válida, como qualquer outra página. |
| Páginas restritas a grupos | Excluídas de todos os artefatos acima, além da pesquisa e do chat de IA, independentemente dos grupos da sessão solicitante (consulte [Acesso baseado em grupos](#acesso-baseado-em-grupos)). |
| API de pesquisa (`/_api/search`) | Sem restrição. Ela se autentica com a sua chave de API `jd_live_` em vez do cookie de login, então mantenha a chave no seu servidor. Páginas restritas por grupo nunca aparecem nos resultados. |
| Servidor MCP (`/_mcp`) | Envie uma chave de API do projeto como `Authorization: Bearer <key>`. Sem ela, o servidor responde com um 401 em JSON em vez do redirecionamento para o login. Veja [Servidor MCP](https://jamdesk.com/docs/pt/ai/mcp-server). |
| Chat de IA | Disponível para visitantes autenticados. Se a sessão expirar, o chat pede ao visitante que recarregue a página e entre novamente. |
| `robots.txt` | Sempre público. Os mecanismos de pesquisa podem ver que existe um site de documentação protegido, mas não podem ver seu conteúdo. |

## Solução de problemas

### Girei a chave de assinatura, mas as sessões antigas ainda parecem funcionar

A rotação e a revogação entram em vigor em cerca de 15 segundos, não imediatamente, porque o gateway de edge armazena temporariamente a configuração de autenticação em cache para manter cada solicitação de página rápida. **Rotate** no dashboard invalida todas as sessões existentes; aguarde até 15 segundos antes de considerar uma sessão antiga ainda válida um problema.

### O dashboard mostra um banner "Runtime out of sync"

O dashboard e o cache do runtime discordam sobre sua chave de assinatura, geralmente porque uma falha temporária de gravação interrompeu uma operação de geração, rotação ou limpeza. O banner informa a situação: ou a chave mais recente ainda não chegou ao cache (tokens assinados com ela podem ser rejeitados), ou uma chave que você limpou ainda está armazenada em cache (tokens assinados com ela continuam sendo aceitos). O Jamdesk verifica novamente sempre que você abre a página de configurações. Se o banner continuar, clique em **Retry sync**. Se isso continuar falhando, faça a rotação da chave ou gere e limpe novamente no caso de uma chave limpa. Um banner que informe que o Jamdesk não pôde verificar o estado significa que a própria verificação falhou; tente novamente quando o runtime estiver acessível.

### Os visitantes recebem um 401 mesmo com um token que sei ser válido

Verifique a claim `host` comparando-a com o host exato solicitado. Se sua documentação estiver acessível tanto em um domínio personalizado (`docs.example.com`) quanto no subdomínio `*.jamdesk.app` subjacente, um token assinado para um deles será rejeitado no outro: a vinculação de `host` é exata e não diferencia maiúsculas de minúsculas, mas não reconhece aliases. Assine tokens para o host que você realmente usa nos links, ou assine duas variantes se usar links para ambos.

### Estou preso em um loop de redirecionamento entre minha página de login e o site de documentação

A rota de callback do Jamdesk recusa-se a redirecionar de volta para si mesma: um valor de `redirect` que aponte para `/_jd/auth/callback` (ou para a página no estilo de desbloqueio abaixo dele) é reescrito para `/` em vez de ser aceito. Se ainda estiver vendo um loop, verifique se seu fluxo de login não está redirecionando para o `loginUrl` da documentação em um ciclo (por exemplo, uma página de login que retorna imediatamente para `/docs-login` quando não encontra uma sessão da documentação). O lado da documentação é protegido; o loop quase sempre está no fluxo de login.

### auth.password e auth.jwt estão ativados

Isso é um `config_error` e bloqueia o build. Escolha um deles; consulte [Migrar da proteção por senha](#migrar-da-proteção-por-senha) para ver a ordem segura das operações durante a mudança.

## Nota de segurança

`apiPlaygroundInputs`, incluindo qualquer valor de Authorization colocado nele, pode ser lido pelo JavaScript executado no seu site de documentação por meio do endpoint de informações da sessão que alimenta o preenchimento antecipado do playground. O preenchimento antecipado é conveniente, mas não é o local adequado para segredos com privilégios elevados.

Envie credenciais por usuário, com o menor privilégio necessário e limitadas ao que esse visitante pode fazer, nunca uma chave de administrador de toda a organização. Trate tudo o que colocar em `apiPlaygroundInputs` como visível para a pessoa que navega pela documentação, porque é isso que acontece.

## Migrar da proteção por senha

Mudar de uma senha compartilhada para a autenticação JWT não exige indisponibilidade, e o site permanece protegido durante todo o processo. Faça isso nesta ordem:

### 1. Gere a chave de assinatura JWT

Faça isso primeiro, enquanto a proteção por senha ainda estiver ativa. Gerar uma chave não altera o que está protegido; a senha continua válida durante todo o processo.

### 2. Altere o docs.json e faça rebuild

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

Faça commit e push. Assim que esse build for publicado, a proteção mudará atomicamente de senha para JWT, sem um período em que o site fique desprotegido. Todas as sessões existentes desbloqueadas por senha terminarão no momento da mudança; a partir daí, os visitantes serão autenticados pelo seu fluxo de login.

### 3. Limpe a senha

Depois de confirmar que o fluxo JWT funciona de ponta a ponta, volte para **Project Settings** e limpe a senha armazenada. Ela está inativa neste momento (o modo de senha está desativado no `docs.json`), mas limpá-la remove completamente o hash armazenado.

## O que vem a seguir?

<Columns cols={2}>
  <Card title="Visão geral do controle de acesso" icon="shield" href="/pt/setup/access-control">
    Compare a autenticação JWT com a proteção por senha, o SSO e o padrão de múltiplos projetos.
  </Card>
  <Card title="Proteção por senha" icon="lock" href="/pt/setup/password-protection">
    A alternativa da senha compartilhada: mais simples de configurar, sem necessidade de integração com backend.
  </Card>
  <Card title="SSO (Enterprise)" icon="key" href="/pt/setup/sso">
    Login orientado por provedor de identidade para clientes corporativos.
  </Card>
  <Card title="Domínios personalizados" icon="globe" href="/pt/deploy/custom-domains">
    Coloque sua documentação no próprio domínio antes de conectar seu fluxo de login.
  </Card>
</Columns>
