simpla.fydocs
Hub API

Autenticação

Como autenticar requisições no Hub API: JWT HS256 de 15 minutos, refresh token de 7 dias com rotation e reuse detection, e Bearer interno para tráfego server-to-server.

Autenticação

O Hub API protege todas as rotas sob https://hub.simplafy.com.br/api/v1 com JWT HS256. Tokens de acesso vivem por 15 minutos e são renovados via refresh token rotativo (7 dias, com detecção de reuso). Para tráfego interno entre serviços, existe um caminho separado por Bearer secret.

Esta página descreve o fluxo end-to-end usado pelo portal web, pelo cliente HTTP (@simplafy/api-client) e por chamadas server-to-server.

Visão geral

ComponenteDetalhe
AlgoritmoHS256 (assinatura simétrica, biblioteca jose)
Access tokenJWT, expira em 900 segundos (15 min), enviado via header
Refresh tokenOpaco (UUID v4), expira em 7 dias, enviado via cookie HttpOnly
RotationA cada /auth/refresh, o token antigo é revogado e um novo é emitido
Reuse detectionReuso de refresh revogado invalida todos os tokens do usuário
Cookierefresh_tokenHttpOnly, SameSite=Strict, Secure em produção, Path=/api/v1/auth/refresh
Auth internaHeader Authorization: Bearer $MCP_INTERNAL_SECRET (comparação timing-safe)

O cookie refresh_token só é enviado pelo navegador para /api/v1/auth/refresh (path-scoped). Endpoints de negócio nunca recebem o refresh — eles só aceitam o access token via header Authorization.

Claims do access token

interface JWTPayload {
  sub: string;                                  // user id
  email: string;
  orgId: string;                                // membership atual
  role: "OWNER" | "ADMIN" | "MEMBER" | "CLIENT";
  permissions: string[];                        // resource:action[] resolvido do role
  iat: number;
  exp: number;
}

O array permissions é resolvido no momento do login/refresh a partir do role (ou de Permission rows quando role = CLIENT). Isso elimina queries de permissão por request — o middleware withAuth lê o claim direto do JWT.

Login (POST /auth/login)

Troca email e senha por um access token e seta o cookie de refresh.

Endpoint: POST https://hub.simplafy.com.br/api/v1/auth/login

Request

{
  "email": "[email protected]",
  "password": "minha-senha-com-pelo-menos-8-chars"
}

Schema (Zod):

const LoginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

O email é normalizado (toLowerCase().trim()) antes da busca.

Response 200

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expiresAt": 1716998400,
  "user": {
    "id": "clx...",
    "email": "[email protected]",
    "name": "User Name",
    "language": "pt-BR"
  },
  "org": {
    "id": "clo...",
    "name": "Minha Organização",
    "role": "ADMIN"
  }
}

expiresAt é um epoch em segundos (sempre now + 900). O cookie refresh_token é setado no mesmo response.

Exemplo

curl -X POST https://hub.simplafy.com.br/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{"email":"[email protected]","password":"sua-senha"}'
const res = await fetch("https://hub.simplafy.com.br/api/v1/auth/login", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  credentials: "include", // preserva o cookie refresh_token
  body: JSON.stringify({ email, password }),
});

const { accessToken, expiresAt, user, org } = await res.json();
import httpx

with httpx.Client() as client:
    r = client.post(
        "https://hub.simplafy.com.br/api/v1/auth/login",
        json={"email": "[email protected]", "password": "sua-senha"},
    )
    r.raise_for_status()
    data = r.json()
    access_token = data["accessToken"]
    # cookies do refresh ficam em client.cookies

Refresh token (rotation)

Endpoint usado pelo cliente quando o access token está perto de expirar (ou já retornou 401).

Endpoint: POST https://hub.simplafy.com.br/api/v1/auth/refresh

O refresh token vem exclusivamente do cookie refresh_token. Não há body nem header customizado. O servidor:

refresh_token do cookie. Sem cookie → 401 No refresh token.

Busca o registro em RefreshToken. Se não existe → 401 Invalid or reused refresh token + apaga o cookie.

Se o token está revogado (revokedAt != null), o servidor entende como reuso e revoga todos os refresh tokens daquele usuário. Retorna 401.

Se o token expirou (expiresAt < now), revoga e retorna 401.

Em uma transação Prisma, marca o token atual como revogado (com replacedBy = novo) e cria um novo refresh token. Emite um novo access token e seta o novo cookie.

Response 200

{
  "accessToken": "eyJhbGciOi...",
  "expiresAt": 1716999300
}

Reuse detection é estrito. Se um cliente legítimo perder a corrida com um atacante e enviar o refresh já rotacionado, ambos são deslogados. O cliente deve guardar apenas o refresh mais recente em cookie e nunca persisti-lo fora do navegador.

Exemplo

curl -X POST https://hub.simplafy.com.br/api/v1/auth/refresh \
  -b cookies.txt \
  -c cookies.txt

No browser, basta fetch(url, { method: "POST", credentials: "include" }) — o navegador anexa o cookie automaticamente porque o path bate.

Headers de auth

Endpoints sob /api/v1/* (exceto /auth/login, /auth/register, /auth/refresh, /auth/forgot-password, /auth/reset-password) exigem:

Authorization: Bearer <accessToken>

Exemplo de chamada autenticada:

curl https://hub.simplafy.com.br/api/v1/auth/me \
  -H "Authorization: Bearer eyJhbGciOi..."

O middleware withAuth decodifica o JWT, valida assinatura e expiração, e injeta um objeto auth no handler:

{
  userId: string;
  email: string;
  orgId: string;
  role: "OWNER" | "ADMIN" | "MEMBER" | "CLIENT";
  permissions: string[];
}

Quando o access expira, o cliente deve chamar /auth/refresh e retentar a request original com o novo token. O SDK @simplafy/api-client já faz isso via interceptor do Axios.

Bearer internal admin

Para chamadas server-to-server entre os apps do monorepo (web → api, mcp-admin → api, proxies internos), existe um caminho de bypass que não usa JWT de usuário.

Como funciona:

Authorization: Bearer <MCP_INTERNAL_SECRET>

A validação é feita por validateInternalAuth em comparação timing-safe (Web Crypto API):

const expected = process.env.MCP_INTERNAL_SECRET;
if (!timingSafeCompare(tokenFromHeader, expected)) {
  // 401
}

O header legado x-internal-admin: true foi removido. Headers controláveis pelo cliente nunca devem dar privilégio — qualquer integração que ainda dependa dele precisa migrar para Authorization: Bearer $MCP_INTERNAL_SECRET.

Onde usar:

  • Apenas em chamadas dentro do cluster (simplafy-prd-hub, simplafy-staging-hub).
  • Nunca expor MCP_INTERNAL_SECRET em frontend, mobile, ou cliente público.
  • Rotacionar via Infisical (/web, /mcp no projeto Hub v1) — após rotação, todos os pods precisam reler o secret.

Erros de auth

StatusCenárioBody
400Payload de login inválido (Zod){ "error": "...validation message..." }
401Credenciais inválidas no login{ "error": "Invalid credentials" }
401Access token ausente, expirado ou assinatura inválida{ "error": "Unauthorized" }
401Cookie refresh_token ausente em /auth/refresh{ "error": "No refresh token" }
401Refresh token inválido, expirado ou reusado{ "error": "Invalid or reused refresh token" }
401Bearer interno ausente, mal formado ou diferente do secret{ "error": "..." }
403Login sem membership de organização{ "error": "User has no organization membership" }
500Erro interno (logado no servidor + GlitchTip){ "error": "Internal server error" }

Boas práticas

  • Trate 401 como dois cenários distintos no cliente: token expirado (retentar via /auth/refresh) vs refresh inválido (forçar logout e voltar para tela de login).
  • Não cache o access token além do necessário — ele já é curto (15 min). Renove proativamente ~1 min antes de expiresAt.
  • Sempre envie credentials: "include" em chamadas para /auth/refresh no browser; sem isso o cookie não trafega.
  • Logout completo: chame POST /api/v1/auth/logout (com access token) — revoga todos os refresh tokens do usuário e apaga o cookie.

On this page