Autenticação & Tokens
O sulfite_server usa um sistema de tokens baseado em HMAC-SHA256. Os tokens são emitidos em pares (access + refresh) com rotação automática e detecção de reuso.
Modos de autenticação
O servidor resolve credenciais nesta ordem:
- Modo dev — se
AUTH_SECRETnão está configurado, todas as requests recebem permissão total. Ideal para desenvolvimento local. - Segredo direto —
Authorization: Bearer <AUTH_SECRET>ouX-API-Key: <AUTH_SECRET>. Se o valor do header é exatamente o segredo configurado (comparação em tempo constante), concede acesso total. - Token assinado — token no formato
base64url(payload).base64url(hmac). Decodificado comoAuthTokencom permissões específicas. - Refresh token rejeitado — tokens do tipo
refreshnão podem ser usados para acesso à API (retorna401). - Sem token / inválido →
401.
Headers aceitos
Authorization: Bearer <token> ou X-API-Key: <token>
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) |
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.canXIsso impede escalação de privilégios — um token consumer não pode emitir um token admin.
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",
"exp": 1712419200,
"perms": { "can_list": true, "can_generate": true },
"jti": "uuid-v4",
"type": "access",
"fid": "uuid-v4"
}| Campo | Obrigatório | Descrição |
|---|---|---|
sub | Não | Subject (identificador do usuário) |
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: canSettings
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
},
"accessTokenLifetime": 900,
"refreshTokenLifetime": 604800
}Todos os campos são opcionais. Sem permissions, usa o preset consumer.
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 |