Skip to content

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:

  1. Modo dev — se AUTH_SECRET não está configurado, todas as requests recebem permissão total. Ideal para desenvolvimento local.
  2. Segredo diretoAuthorization: Bearer <AUTH_SECRET> ou X-API-Key: <AUTH_SECRET>. Se o valor do header é exatamente o segredo configurado (comparação em tempo constante), concede acesso total.
  3. Token assinado — token no formato base64url(payload).base64url(hmac). Decodificado como AuthToken com permissões específicas.
  4. Refresh token rejeitado — tokens do tipo refresh não podem ser usados para acesso à API (retorna 401).
  5. Sem token / inválido401.

Headers aceitos

Authorization: Bearer <token> ou X-API-Key: <token>

Modelo de permissões

Cada token carrega um conjunto de permissões booleanas:

PermissãoDefaultControla
can_listtrueGET /reports, GET /consumer/reports
can_insertfalsePOST /reports
can_editfalsePUT /reports/:id
can_deletefalseDELETE /reports/:id
can_generatetruePOST /generate*, consumer output
can_settingsfalse/connections, /datasources/introspect, /tokens, /health/security
can_sharefalsePOST /reports/:id/share (futuro)

Presets de permissão

Presetlistinserteditdeletegeneratesettingsshare
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.

Formato do token

O token não é um JWT padrão. O formato é:

base64url(json_payload).base64url(HMAC-SHA256(payload, secret))

Payload:

json
{
  "sub": "user@example.com",
  "exp": 1712419200,
  "perms": { "can_list": true, "can_generate": true },
  "jti": "uuid-v4",
  "type": "access",
  "fid": "uuid-v4"
}
CampoObrigatórioDescrição
subNãoSubject (identificador do usuário)
expNãoExpiração (epoch seconds UTC)
permsSimPermissões do token
jtiNãoID único do token
typeSimaccess, refresh ou legacy
fidNãoID 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:

json
{
  "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:

json
{
  "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"
}
StatusErroQuando
403forbiddenSem canSettings

Rotacionar — POST /api/v1/tokens/refresh

Autenticação: NÃO obrigatória. O refresh token no body é a credencial.

Body:

json
{ "refreshToken": "base64url.signature" }

Resposta 200: Mesmo shape da emissão.

StatusErroQuando
400validation_errorrefreshToken ausente
401unauthorizedAssinatura inválida, não é refresh, sem JTI, não encontrado
401token_reuse_detectedToken revogado reutilizado → família inteira revogada
401token_expiredExpiração absoluta ou sliding excedida

Revogar — POST /api/v1/tokens/revoke

Permissão: canSettings

Body (um dos três):

json
{ "refreshToken": "base64url.signature" }
json
{ "jti": "uuid-v4" }
json
{ "subject": "user@example.com" }

Resposta 200:

json
{
  "revoked": true,
  "count": 3,
  "familiesAffected": 1
}
StatusErroQuando
400validation_errorNenhum dos três campos fornecido
403forbiddenSem canSettings
422invalid_tokenNão foi possível decodificar o refresh token

Verificar — POST /api/v1/tokens/verify

Permissão: canSettings

Body:

json
{ "token": "base64url.signature" }

Resposta 200:

json
{
  "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 }
}
StatusErroQuando
400validation_errortoken ausente
422invalid_tokenToken inválido, adulterado ou expirado

Listar ativos — GET /api/v1/tokens

Permissão: canSettings

Query params: ?subject=user@example.com (filtro opcional)

Resposta 200:

json
{
  "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ávelDefaultLimites
ACCESS_TOKEN_LIFETIME900s (15 min)Clamped: 60–3600s
REFRESH_TOKEN_LIFETIME604800s (7 dias)Clamped: 60–2592000s
REFRESH_FAMILY_MAX_LIFETIME2592000s (30 dias)Limite absoluto da cadeia de rotação

Token store

ImplementaçãoPara quêConfiguração
InMemoryTokenStoreDesenvolvimento / testesPadrão (sem env)
PersistentTokenStoreProduçãoTOKEN_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:

EventoQuando
token_issuedNovo par de tokens criado
token_refreshedToken rotacionado
token_revokedToken individual revogado
token_reuse_detectedRefresh token revogado reutilizado
family_revokedFamília inteira revogada
subject_revokedTodos os tokens de um subject revogados
auth_failedFalha de autenticação

Sulfite do 🇧🇷 para o mundo © 2026 Rafael S. Pinheiro