Logosulfite.app
rafagazani/sulfite 999999

Autenticação & Tokens

Sistema de tokens, permissões e autenticação do sulfite_server

Autenticação & Tokens#

O sulfite_server suporta quatro modos de autenticação. Escolha o que melhor se encaixa no seu cenário:

ModoQuando usar
Dev modeDesenvolvimento 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)
`AUTH_DISABLED=true` concede acesso total a qualquer request sem validação. Defina `AUTH_DISABLED=false` explicitamente em produção para evitar ativação acidental.

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:

  1. Dev mode ativo → superuser
  2. POST /tokens/refresh → bypass (auto-autenticado pelo body)
  3. Token é exatamente o AUTH_SECRETsuperAdmin
  4. Token passa na verificação HMAC → permissões do payload
  5. AUTH_JWKS_URL configurado e token passa na validação RS256 → permissões mapeadas do JWT
  6. Nenhum passou → 401 unauthorized

Tokens do tipo refresh são rejeitados em qualquer endpoint que não seja /tokens/refresh.

`Authorization: Bearer ` ou `X-API-Key: ` — ambos funcionam em qualquer modo.

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)
can_edit_globalfalseEscrita em templates globais
can_adminfalseRotas administrativas multi-tenant
Quando a view `sulfite_authorized_users` existe no banco do tenant, essas permissões são intersectadas com a linha do usuário na view. Veja [Autorização por tenant](/server/tenant-authorization).

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.

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.

Se o `AUTH_SECRET` vazar, qualquer pessoa pode emitir tokens com qualquer permissão. Mantenha-o em um secret manager (Doppler, AWS Secrets Manager, Vault) e nunca o exponha em logs, respostas de API ou código-fonte.

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ávelDefaultDescrição
AUTH_JWKS_CACHE_TTL_SECONDS300TTL do cache de chaves públicas
AUTH_JWKS_FETCH_TIMEOUT_SECONDS10Timeout 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
CampoObrigatórioDescrição
subNãoSubject (identificador do usuário)
emailNãoEmail usado pela autorização por tenant
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: 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"
}
StatusErroQuando
403forbiddenSem 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.

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):

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

Resposta 200:

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

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

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