Logosulfite.app
rafagazani/sulfite 999999

Hub externo — autenticação e conexões

Como usar JWT RS256 via JWKS e registry remoto de conexões com qualquer hub externo

Hub externo — autenticação e conexões#

Use este padrão quando outro sistema já é responsável por autenticar usuários e/ou gerenciar credenciais de banco por tenant. O Sulfite se integra a qualquer hub que implemente os contratos descritos aqui — a linguagem ou stack do hub não importa.

Configure `AUTH_JWKS_URL` para aceitar JWT RS256 do seu IdP, `CONNECTION_REGISTRY_BACKEND=remote` para resolver conexões no hub e `CONNECTION_ROUTES_ENABLED=false` para remover o CRUD de conexões do Sulfite.

Quando usar#

  • Seu sistema já autentica usuários e emite JWT — você não quer duplicar login no Sulfite.
  • Cada tenant tem seu próprio banco, cujas credenciais ficam gerenciadas centralmente.
  • O Sulfite deve executar relatórios sem armazenar senhas dos bancos dos tenants.
  • Múltiplas réplicas do sulfite_server precisam funcionar sem estado local compartilhado.

Arquitetura#

App cliente
   │
   │ 1. login(usuario, senha, tenant_id)
   ▼
Hub de autenticação (qualquer stack)
   │
   │ 2. emite JWT RS256 com tenant_id e permissões
   ▼
App cliente
   │
   │ 3. POST /api/v1/reports/:id/generate
   │    Authorization: Bearer <jwt>
   ▼
sulfite_server
   │
   ├─ valida JWT via JWKS do hub
   ├─ verifica tenant e permissões
   ├─ carrega o relatório no storage do Sulfite
   ├─ resolve connectionRef no hub de conexões
   └─ executa datasource e renderiza PDF/HTML/CSV/Excel

O Sulfite mantém relatórios e metadados no seu próprio storage. Os dados de negócio ficam nos bancos dos tenants, acessados com credenciais fornecidas pelo hub em runtime.

Configuração#

# Auth externo via JWT RS256/JWKS
AUTH_JWKS_URL=https://auth.your-system.example.com/.well-known/jwks.json
AUTH_ISSUER=https://auth.your-system.example.com
AUTH_AUDIENCE=sulfite-api
AUTH_TENANT_CLAIM=tenant_id

# Conexões resolvidas pelo hub
CONNECTION_REGISTRY_BACKEND=remote
CONNECTION_HUB_URL=https://hub.your-system.internal
CONNECTION_HUB_TOKEN=<credencial-de-servico>
CONNECTION_HUB_TIMEOUT_MS=3000
CONNECTION_HUB_CACHE_TTL_SECONDS=60
CONNECTION_ROUTES_ENABLED=false

# Multi-tenancy e storage do Sulfite
MULTI_TENANT_MODE=true
STORAGE_BACKEND=postgres
DATABASE_URL=postgresql://sulfite:secret@postgres:5432/sulfite_reports

# Recomendado em produção
SULFITE_CLUSTER_MODE=true
ENCRYPTION_KEY=<openssl rand -base64 32>
VariávelObrigatóriaDescrição
AUTH_JWKS_URL Sim URL pública do JWKS do hub de autenticação
AUTH_ISSUER Não Valor esperado no claim iss
AUTH_AUDIENCE Não Valor esperado no claim aud
AUTH_JWKS_CACHE_TTL_SECONDS Não TTL do cache de chaves JWKS. Default: 300
AUTH_JWKS_FETCH_TIMEOUT_SECONDS Não Timeout para buscar JWKS. Default: 10
AUTH_TENANT_CLAIM Não Claim que contém o tenant. Default: tenant_id
CONNECTION_REGISTRY_BACKEND Sim Use remote
CONNECTION_HUB_URL Sim URL interna do hub de conexões
CONNECTION_HUB_TOKEN Sim Credencial de serviço usada pelo Sulfite
CONNECTION_HUB_TIMEOUT_MS Não Timeout para chamadas ao hub. Default: 3000
CONNECTION_HUB_CACHE_TTL_SECONDS Não TTL do cache de conexões. Default: 60
CONNECTION_ROUTES_ENABLED Não Use false para desabilitar /connections
`CONNECTION_HUB_URL` deve apontar para uma rota interna ou protegida por rede privada. O token de serviço é uma segunda camada de proteção, não substitui firewall ou NetworkPolicy bloqueando `/internal/*`.

Contrato do JWT#

O hub de autenticação deve emitir JWT assinado com RS256 e publicar as chaves públicas em um endpoint JWKS. O header do JWT precisa conter kid.

Payload esperado:

{
  "sub": "usuario@example.com",
  "email": "usuario@example.com",
  "iss": "https://auth.your-system.example.com",
  "aud": "sulfite-api",
  "exp": 1777065600,
  "tenant_id": "tenant-a-uuid",
  "permissions": {
    "reports": {
      "list": true,
      "generate": true,
      "create": false,
      "edit": false,
      "delete": false,
      "settings": false,
      "share": false,
      "edit_global": false,
      "admin": false
    }
  }
}

Mapeamento de claims para o token interno do Sulfite:

Claim do hubCampo no Sulfite
subAuthToken.subject
emailAuthToken.email
tenant_id ou tntAuthToken.tenantId
permissions.reports.listcanList
permissions.reports.generatecanGenerate
permissions.reports.createcanInsert
permissions.reports.editcanEdit
permissions.reports.deletecanDelete
permissions.reports.settingscanSettings
permissions.reports.sharecanShare
permissions.reports.edit_globalcanEditGlobal
permissions.reports.admincanAdmin

Se o token for inválido, expirado, assinado por uma chave desconhecida ou tiver iss/aud divergente, a API retorna 401.

Se você usar a view `sulfite_authorized_users`, inclua a claim `email` no JWT. O Sulfite usa esse email para buscar a linha do usuário no banco do tenant. Veja [Autorização por tenant](/server/tenant-authorization).

Contrato do endpoint de conexões#

Quando um relatório usa um datasource com connectionRef, o Sulfite chama:

GET /internal/tenants/{tenantId}/connections/{connectionRef}
Authorization: Bearer <CONNECTION_HUB_TOKEN>

Resposta para conexão de banco:

{
  "type": "database",
  "name": "erp_main",
  "host": "db.tenant-a.internal",
  "port": 5432,
  "database": "erp",
  "username": "readonly",
  "password": "secret",
  "ssl": true
}

Resposta para conexão HTTP:

{
  "type": "http",
  "name": "api_fiscal",
  "baseUrl": "https://api-fiscal.tenant-a.internal",
  "defaultHeaders": { "X-Api-Key": "secret" }
}
Status do hubResultado no Sulfite
200Conexão cacheada e usada na geração
404resolveAsync retorna null
403 Geração retorna 403 connection_forbidden
401 Geração retorna 503 connection_unavailable
timeout / 5xx Geração retorna 503 connection_unavailable

Implementando o endpoint no hub#

Os parâmetros tenantId e connectionRef chegam como segmentos de path URL-encoded, prevenindo path traversal. O hub só precisa:

  1. Verificar o token Bearer no header Authorization
  2. Buscar as credenciais pelo tenantId + name
  3. Retornar o JSON no formato acima, ou 404 se não encontrado
GET /internal/tenants/tenant-a-uuid/connections/erp_main
Authorization: Bearer <CONNECTION_HUB_TOKEN>

Cache e aquecimento#

O RemoteConnectionRegistry mantém cache em memória por tenantId/connectionRef. Antes de executar o relatório, o GenerateHandler aquece as conexões usadas por:

  • DataSource.connectionRef;
  • DbLookupConfig.connectionRef em parâmetros de relatório.

Esse aquecimento é necessário porque os resolvers de datasource usam a API síncrona ConnectionRegistry.resolve(). O fluxo:

generate request
  ├─ parse report definition
  ├─ coleta connectionRef dos datasources e lookups
  ├─ await registry.resolveAsync(tenantId, connectionRef)   ← chamada ao hub
  ├─ cache preenchido
  └─ engine.generate() usa registry.resolve(connectionRef)  ← lê do cache

Em jobs assíncronos (Prefer: respond-async), o aquecimento é repetido dentro do executor do job, porque o cache da request HTTP original pode expirar antes da execução.

Desabilitando o CRUD de conexões#

Quando o hub é a única fonte de verdade para credenciais, desabilite as rotas de conexão do Sulfite:

CONNECTION_ROUTES_ENABLED=false

Isso remove do roteador:

GET    /api/v1/connections
POST   /api/v1/connections
GET    /api/v1/connections/:name
PUT    /api/v1/connections/:name
DELETE /api/v1/connections/:name
POST   /api/v1/connections/:name/test

Mesmo com as rotas desligadas, mantenha permissions.reports.settings=false nos JWTs de usuários finais.

Testando a integração#

# Valide o JWKS do hub
curl https://auth.your-system.example.com/.well-known/jwks.json

# Gere um relatório com JWT do hub
curl -X POST "https://sulfite.example.com/api/v1/reports/<id>/generate?format=pdf" \
  -H "Authorization: Bearer $HUB_JWT" \
  -o report.pdf

# Confirme que o CRUD de conexões está desabilitado
curl -i https://sulfite.example.com/api/v1/connections \
  -H "Authorization: Bearer $HUB_JWT"
# Esperado: 404

Checklist de produção#

  • Publique JWKS com rotação de chaves mantendo as antigas até todos os tokens expirarem.
  • Use exp curto nos JWTs do hub.
  • Configure AUTH_ISSUER e AUTH_AUDIENCE em produção.
  • Mantenha /internal/* acessível apenas ao sulfite_server.
  • Use CONNECTION_HUB_TOKEN como credencial de serviço, nunca o token do usuário final.
  • Desabilite CONNECTION_ROUTES_ENABLED quando conexões forem gerenciadas exclusivamente pelo hub.
  • Use STORAGE_BACKEND=postgres e SULFITE_CLUSTER_MODE=true em múltiplas réplicas.
  • Nunca registre senha de conexão ou token de serviço em logs.

Ver também#