Autenticação & Tokens#
O sulfite_server suporta quatro modos de autenticação. Escolha o que melhor se encaixa no seu cenário:
| Modo | Quando usar |
|---|---|
| Dev mode | Desenvolvimento local — sem configurar nenhum secret |
| Segredo direto | Integração server-to-server, CI/CD, scripts de bootstrap |
| Token HMAC | O Sulfite é o único sistema de autenticação da sua stack |
| JWT RS256 via JWKS | Você já tem um IdP externo emitindo tokens (Auth0, Keycloak, Supabase, hub próprio) |
Dev mode#
Quando usar: desenvolvimento local, testes de integração, ambientes sem dados sensíveis.
AUTH_DISABLED=true
Toda request recebe superuser (permissões totais) sem nenhuma validação. Requer opt-in explícito —
AUTH_SECRET ausente não ativa dev mode, retorna 401.
Segredo direto (super-admin)#
Quando usar: scripts de administração, CI/CD, bootstrap inicial de tokens, chamadas server-to-server sem gestão de sessão.
AUTH_SECRET=<openssl rand -base64 32>
Apresente o valor exato do AUTH_SECRET no header:
Authorization: Bearer <AUTH_SECRET>
X-API-Key: <AUTH_SECRET>
A comparação usa tempo constante (sem timing attack). Concede acesso total (superAdmin). Não use como credencial de usuário final — prefira emitir tokens com permissões restritas via
POST /api/v1/tokens.
Token HMAC (autenticação própria)#
Quando usar: o Sulfite é o sistema de autenticação da sua aplicação — você emite tokens para seus usuários diretamente via
POST /api/v1/tokens.
AUTH_SECRET=<openssl rand -base64 32>
Tokens são emitidos em pares access + refresh com rotação automática e detecção de reuso. O formato é próprio (não é JWT padrão):
base64url(json_payload).base64url(HMAC-SHA256(payload, secret))
Cada token carrega permissões granulares. Veja Emissão de tokens abaixo.
JWT RS256 via JWKS (IdP externo)#
Quando usar: você já tem um sistema de login (Auth0, Keycloak, Supabase, hub próprio) e quer que o Sulfite aceite os tokens que ele emite — sem duplicar autenticação.
AUTH_JWKS_URL=https://auth.your-system.example.com/.well-known/jwks.json
AUTH_ISSUER=https://auth.your-system.example.com # recomendado
AUTH_AUDIENCE=sulfite-api # recomendado
AUTH_SECRET e AUTH_JWKS_URL são mutuamente exclusivos.
O Sulfite busca a chave pública pelo kid no header do JWT, verifica a assinatura RS256, confere
iss/aud, e mapeia permissions.reports.* para permissões internas. Veja
exemplos por IdP e Hub externo para o contrato completo.
Ordem de resolução#
O middleware avalia cada request nesta sequência — para no primeiro que passar:
- Dev mode ativo →
superuser POST /tokens/refresh→ bypass (auto-autenticado pelo body)- Token é exatamente o
AUTH_SECRET→superAdmin - Token passa na verificação HMAC → permissões do payload
AUTH_JWKS_URLconfigurado e token passa na validação RS256 → permissões mapeadas do JWT- Nenhum passou →
401 unauthorized
Tokens do tipo refresh são rejeitados em qualquer endpoint que não seja /tokens/refresh.
Modelo de permissões#
Cada token carrega um conjunto de permissões booleanas:
| Permissão | Default | Controla |
|---|---|---|
can_list | true | GET /reports, GET /consumer/reports |
can_insert | false | POST /reports |
can_edit | false | PUT /reports/:id |
can_delete | false | DELETE /reports/:id |
can_generate | true | POST /generate*, consumer output |
can_settings | false | /connections, /datasources/introspect, /tokens, /health/security |
can_share | false | POST /reports/:id/share (futuro) |
can_edit_global | false | Escrita em templates globais |
can_admin | false | Rotas administrativas multi-tenant |
Presets de permissão#
| Preset | list | insert | edit | delete | generate | settings | share |
|---|---|---|---|---|---|---|---|
| admin | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| consumer | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
| readOnly | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
Capping de permissões#
Tokens emitidos nunca excedem as permissões do emissor. Cada campo é um AND lógico:
granted.canX = requested.canX && issuer.canX
Isso impede escalação de privilégios — um token consumer não pode emitir um token admin.
Segurança da emissão de tokens#
Quem pode emitir tokens?#
POST /api/v1/tokens exige can_settings. Nenhum preset padrão de usuário final tem essa permissão — apenas admin. Isso significa que um usuário comum, mesmo autenticado, recebe 403 se tentar chamar esse endpoint.
Além disso, o endpoint só existe quando AUTH_SECRET está configurado. No modo JWKS (IdP externo), as rotas /tokens não são registradas — tokens vêm exclusivamente do seu IdP.
Bootstrap: como emitir o primeiro token#
O AUTH_SECRET é a raiz de confiança. Para iniciar o sistema:
# 1. Use AUTH_SECRET diretamente como Bearer (acesso super-admin)
curl -X POST https://sulfite.example.com/api/v1/tokens \
-H "Authorization: Bearer $AUTH_SECRET" \
-H "Content-Type: application/json" \
-d '{
"subject": "backend-service",
"permissions": {
"can_settings": true,
"can_list": true,
"can_generate": true,
"can_edit_global": true
}
}'
# 2. Guarde o accessToken retornado — use-o no seu backend para emitir tokens consumer
Depois disso, o AUTH_SECRET fica guardado no secret manager e não precisa mais ser usado diretamente.
Delegação: backend emite tokens para usuários finais#
O padrão recomendado é o backend da sua aplicação emitir tokens com permissões mínimas:
AUTH_SECRET (root)
└─ emite token admin para o backend
└─ backend emite tokens consumer para cada usuário
└─ usuário acessa /generate, /reports — mas não /tokens
# Backend emite token consumer para um usuário que fez login
curl -X POST https://sulfite.example.com/api/v1/tokens \
-H "Authorization: Bearer $BACKEND_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject": "user-123",
"permissions": { "can_list": true, "can_generate": true },
"accessTokenLifetime": 900,
"refreshTokenLifetime": 86400
}'
O token gerado não tem can_settings — o usuário final não pode emitir outros tokens, revogar sessões alheias, nem acessar conexões.
Formato do token#
O token não é um JWT padrão. O formato é:
base64url(json_payload).base64url(HMAC-SHA256(payload, secret))
Payload:
{
"sub": "user@example.com",
"email": "user@example.com",
"exp": 1712419200,
"perms": { "can_list": true, "can_generate": true },
"jti": "uuid-v4",
"type": "access",
"fid": "uuid-v4"
}
Exemplos por IdP#
Auth0#
AUTH_JWKS_URL=https://your-tenant.auth0.com/.well-known/jwks.json
AUTH_ISSUER=https://your-tenant.auth0.com/
AUTH_AUDIENCE=https://your-api-identifier
O JWT emitido pelo Auth0 deve conter tenant_id (ou o valor de AUTH_TENANT_CLAIM) para roteamento multi-tenant.
Keycloak#
AUTH_JWKS_URL=https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs
AUTH_ISSUER=https://keycloak.example.com/realms/myrealm
AUTH_AUDIENCE=sulfite-api
Supabase#
AUTH_JWKS_URL=https://your-project.supabase.co/auth/v1/jwks
AUTH_ISSUER=https://your-project.supabase.co/auth/v1
Cache e resiliência#
| Variável | Default | Descrição |
|---|---|---|
AUTH_JWKS_CACHE_TTL_SECONDS | 300 | TTL do cache de chaves públicas |
AUTH_JWKS_FETCH_TIMEOUT_SECONDS | 10 | Timeout HTTP para buscar JWKS |
Em caso de falha na busca das chaves (5xx, timeout), o cache expirado é mantido para evitar downtime. Uma "negative cache" evita retry storm após falha.
Testando#
# Obtenha um token do seu IdP
TOKEN=$(curl -s -X POST https://your-tenant.auth0.com/oauth/token \
-H "content-type: application/json" \
-d '{"client_id":"...","client_secret":"...","audience":"...","grant_type":"client_credentials"}' \
| jq -r .access_token)
# Verifique o endpoint
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/reports
| Campo | Obrigatório | Descrição |
|---|---|---|
sub | Não | Subject (identificador do usuário) |
email | Não | Email usado pela autorização por tenant |
exp | Não | Expiração (epoch seconds UTC) |
perms | Sim | Permissões do token |
jti | Não | ID único do token |
type | Sim | access, refresh ou legacy |
fid | Não | ID da família (apenas refresh tokens) |
Ciclo de vida do refresh token#
1. Emissão#
POST /tokens cria um par access + refresh. O refresh token recebe um familyId (UUID v4) e um absoluteExpiresAt (agora + 30 dias).
2. Rotação#
POST /tokens/refresh troca o refresh antigo por um novo par. O token antigo é marcado como revoked com replacedBy: newJti. O novo herda o familyId e absoluteExpiresAt.
3. Detecção de reuso#
Se um refresh token já revogado é apresentado, toda a família é revogada. Retorna 401 token_reuse_detected. Um evento de auditoria é emitido.
4. Expiração absoluta#
A família não pode ser estendida além de absoluteExpiresAt (30 dias desde a emissão inicial).
5. Limpeza#
Entradas revogadas/expiradas e mais velhas que 24h são removidas automaticamente.
Endpoints#
Emitir token — POST /api/v1/tokens#
Permissão: can_settings — veja Segurança da emissão para entender quem pode chamar este endpoint e o fluxo de bootstrap recomendado.
Body:
{
"subject": "user@example.com",
"permissions": {
"can_list": true,
"can_insert": false,
"can_edit": false,
"can_delete": false,
"can_generate": true,
"can_settings": false,
"can_share": false,
"can_edit_global": false
},
"accessTokenLifetime": 900,
"refreshTokenLifetime": 604800
}
Todos os campos são opcionais. Sem permissions, usa o preset consumer. As permissões concedidas são limitadas pelas permissões do emissor — não é possível emitir um token com mais acesso do que o próprio token usado na chamada.
Resposta 201:
{
"accessToken": "base64url.signature",
"refreshToken": "base64url.signature",
"accessTokenExpiresAt": "2026-04-06T12:15:00.000Z",
"refreshTokenExpiresAt": "2026-04-13T12:00:00.000Z",
"absoluteExpiresAt": "2026-05-06T12:00:00.000Z",
"tokenType": "Bearer"
}
| Status | Erro | Quando |
|---|---|---|
403 | forbidden | Sem canSettings |
Rotacionar — POST /api/v1/tokens/refresh#
Autenticação: NÃO obrigatória. O refresh token no body é a credencial.
Body:
{ "refreshToken": "base64url.signature" }
Resposta 200: Mesmo shape da emissão.
| Status | Erro | Quando |
|---|---|---|
400 | validation_error | refreshToken ausente |
401 | unauthorized | Assinatura inválida, não é refresh, sem JTI, não encontrado |
401 | token_reuse_detected | Token revogado reutilizado → família inteira revogada |
401 | token_expired | Expiração absoluta ou sliding excedida |
Revogar — POST /api/v1/tokens/revoke#
Permissão: canSettings
Body (um dos três):
{ "refreshToken": "base64url.signature" }
{ "jti": "uuid-v4" }
{ "subject": "user@example.com" }
Resposta 200:
{
"revoked": true,
"count": 3,
"familiesAffected": 1
}
| Status | Erro | Quando |
|---|---|---|
400 | validation_error | Nenhum dos três campos fornecido |
403 | forbidden | Sem canSettings |
422 | invalid_token | Não foi possível decodificar o refresh token |
Verificar — POST /api/v1/tokens/verify#
Permissão: canSettings
Body:
{ "token": "base64url.signature" }
Resposta 200:
{
"subject": "user@example.com",
"expiresAt": "2026-04-06T12:15:00.000Z",
"expired": false,
"type": "access",
"jti": "uuid-v4",
"familyId": "uuid-v4",
"permissions": { "can_list": true }
}
| Status | Erro | Quando |
|---|---|---|
400 | validation_error | token ausente |
422 | invalid_token | Token inválido, adulterado ou expirado |
Listar ativos — GET /api/v1/tokens#
Permissão: canSettings
Query params: ?subject=user@example.com (filtro opcional)
Resposta 200:
{
"tokens": [
{
"jti": "uuid-v4",
"family_id": "uuid-v4",
"subject": "user@example.com",
"permissions": { "can_list": true },
"issued_at": "2026-04-06T12:00:00.000Z",
"expires_at": "2026-04-13T12:00:00.000Z",
"absolute_expires_at": "2026-05-06T12:00:00.000Z",
"revoked": false
}
]
}
Configuração de lifetime#
| Variável | Default | Limites |
|---|---|---|
ACCESS_TOKEN_LIFETIME | 900s (15 min) | Clamped: 60–3600s |
REFRESH_TOKEN_LIFETIME | 604800s (7 dias) | Clamped: 60–2592000s |
REFRESH_FAMILY_MAX_LIFETIME | 2592000s (30 dias) | Limite absoluto da cadeia de rotação |
Token store#
| Implementação | Para quê | Configuração |
|---|---|---|
InMemoryTokenStore | Desenvolvimento / testes | Padrão (sem env) |
PersistentTokenStore | Produção | TOKEN_STORE_PATH=./data/tokens.json |
O store persistente salva em JSON com permissão chmod 600.
Auditoria#
Eventos de autenticação são logados como JSON estruturado no stdout:
| Evento | Quando |
|---|---|
token_issued | Novo par de tokens criado |
token_refreshed | Token rotacionado |
token_revoked | Token individual revogado |
token_reuse_detected | Refresh token revogado reutilizado |
family_revoked | Família inteira revogada |
subject_revoked | Todos os tokens de um subject revogados |
auth_failed | Falha de autenticação |