Autenticação
Os dois modos de autenticação Keycloak — usuário final (/api) e app-to-app (/service).
A API valida JWTs emitidos pelo Keycloak usando JWKS (chaves públicas
buscadas remotamente e cacheadas pela lib jose). Há dois middlewares distintos,
montados por prefixo de rota no AppModule:
| Prefixo | Middleware | Popula | Emissor validado |
|---|---|---|---|
/api/* | KeycloakAuthMiddleware | req.user | KEYCLOAK_REALM_URL |
/service/* | ServiceAuthMiddleware | req.service | KEYCLOAK_AUTH_URL |
Ambos exigem o header Authorization: Bearer <token>. Sem ele, ou com token
inválido/expirado, a resposta é 401 Unauthorized.
Fluxo de usuário final (/api/*)
Destinado a usuários humanos autenticados no realm do Keycloak. O middleware:
- Extrai e verifica o JWT contra o JWKS do realm (
KEYCLOAK_REALM_URL). - Lê o
sub(Keycloak subject) do token. - Consulta o cache Redis (
user_auth:<sub>, TTL de 5 min). Em hit, usa o usuário cacheado. - Em miss, resolve o usuário local via
idIdentity = sube carrega suas permissões (UMA) e a flagisSuperAdmin(role de realmsuper_admin).
req.user (tipo IUser) carrega, entre outros, o id do usuário local, suas
permissions e isSuperAdmin — consumidos pelo PermissionGuard.
Tenant ativo (X-Tenant-Id) e persona por tenant
A persona do usuário (tenant-admin/coordinator/technician/viewer) é por tenant — vive
em UserTenant.persona (banco). A persona global também vive no banco — User.type === 'Global' — e não em nenhuma role do token: nenhuma persona é atribuída via Keycloak. Por isso
toda request de usuário não-global precisa resolver um tenant ativo antes de decidir o que
ele pode fazer: o escopo de leitura (cooperativas alcançadas pelos tenants do usuário,
allowedOrganizationIds) e a persona vêm os dois do mesmo tenant resolvido
(resolveTenantScope). global é o único caso que não precisa de tenant nenhum.
O header X-Tenant-Id escolhe o tenant ativo — obrigatório na prática para quem não é
global, exceto quando dá pra resolver sozinho:
| Situação | Header | Efeito |
|---|---|---|
global | ausente ou vazio | opcional — segue sem tenant ativo, escopo é a união de todas as cooperativas |
não-global, só 1 tenant | ausente ou vazio | resolvido automaticamente — não precisa mandar o header |
não-global, 0 ou 2+ tenants | ausente ou vazio | 403 Forbidden — ambíguo demais para adivinhar, exige seleção explícita |
| qualquer persona | tenant com membership ativa e persona atribuída | escopo restrito às cooperativas daquele tenant, persona resolvida daquele vínculo |
| qualquer persona | tenant sem membership ativa, ou membership sem persona atribuída | 403 Forbidden — nunca cai de volta na união |
curl http://localhost:3000/api/organizations \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-Tenant-Id: t1b2c3d4-e5f6-4789-a123-456789abcdef"Consequências para quem consome a API com um tenant ativo:
- filtrar explicitamente por uma
organizationIdfora do tenant ativo →403; GET /api/<recurso>/:idde um registro fora do tenant ativo →404;- tenant ativo sem nenhuma cooperativa vinculada → lista vazia (não vira união);
- vínculo (
UserTenant) sempersonaatribuída (ex.: criado antes da migração de persona por tenant, ainda não reatribuído) →403, mesmo que o usuário pertença ao tenant.
O painel lista os tenants elegíveis em GET /api/me/tenants. Rotas
/service/* não são afetadas: o tenant já vem resolvido pelo azp do token de serviço.
Exceção de bootstrap (RJGDD-16). A linha "0 ou 2+ tenants → 403" acima não vale para
GET /api/me/tenantseGET /api/me/permissions— sem essa exceção, ninguém com 0 ou 2+ tenants conseguiria carregar a própria lista de tenants pra escolher um no seletor (a chamada que alimenta o seletor exigiria um tenant já selecionado). Esses dois endpoints resolvem sem tenant/persona em vez de403; todas as outras rotas/api/*continuam fail-closed como a tabela descreve.
Fluxo app-to-app (/service/*)
Destinado a comunicação entre serviços usando client credentials do Keycloak. O middleware:
- Verifica o JWT contra o JWKS de
KEYCLOAK_AUTH_URL. - Lê o
azp(authorized party =client_idque chamou, ex.:app1-tenant-xyz). - Resolve o tenant correspondente àquele client (via
keycloakClientId), populandoreq.servicecom o contexto de serviço (tenantId, escopos). - Resolve as organizações vinculadas a esse tenant (
OrganizationTenant) e liga o escopo por organização no contexto da request — ver abaixo.
Rotas /service/* não usam o PermissionGuard baseado em resource/scope
do usuário; a autorização se dá pela identidade do client (tenant) e suas
organizações vinculadas.
Escopo por organização
Um token de serviço só alcança as organizações vinculadas ao seu tenant (tabela
organization_tenants). O middleware carrega esses IDs e ativa o mesmo motor de escopo usado nas
rotas de usuário, então o filtro é aplicado na camada de banco, não endpoint a endpoint:
- Listagens (
GET /service/documents,GET /service/organizations) retornam apenas o que está dentro do vínculo — sem precisar de nenhum filtro do chamador. - Pedido explícito fora do vínculo (
?organizationId=de outra organização) →403. - Busca por id de um registro fora do vínculo →
404, como se não existisse. - Tenant sem nenhum vínculo não enxerga nada (fail-closed) — o comportamento seguro por padrão.
Três rotas são exceção deliberada, porque é através delas que o vínculo nasce:
GET /service/organizations/federal-tax-id/:federalTaxId (busca por CNPJ e vincula),
POST /service/organizations (cria e vincula) e
POST /service/organizations/:organizationId/tenants (vincula). Elas consultam a base inteira
por definição.
Cache de autenticação
O contexto de usuário é cacheado no Redis por 5 minutos
(USER_CACHE_TTL_SECONDS). Alterações de permissão no Keycloak podem levar até
esse intervalo para refletir na API.
A chave inclui o sub, o azp (as roles são por aplicação) e o X-Tenant-Id recebido — o
escopo depende do tenant ativo, então cada combinação tem sua própria entrada e um contexto
não vaza para requisições com outro tenant.