Profile Server API

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:

PrefixoMiddlewarePopulaEmissor validado
/api/*KeycloakAuthMiddlewarereq.userKEYCLOAK_REALM_URL
/service/*ServiceAuthMiddlewarereq.serviceKEYCLOAK_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:

  1. Extrai e verifica o JWT contra o JWKS do realm (KEYCLOAK_REALM_URL).
  2. Lê o sub (Keycloak subject) do token.
  3. Consulta o cache Redis (user_auth:<sub>, TTL de 5 min). Em hit, usa o usuário cacheado.
  4. Em miss, resolve o usuário local via idIdentity = sub e carrega suas permissões (UMA) e a flag isSuperAdmin (role de realm super_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çãoHeaderEfeito
globalausente ou vazioopcional — segue sem tenant ativo, escopo é a união de todas as cooperativas
não-global, só 1 tenantausente ou vazioresolvido automaticamente — não precisa mandar o header
não-global, 0 ou 2+ tenantsausente ou vazio403 Forbidden — ambíguo demais para adivinhar, exige seleção explícita
qualquer personatenant com membership ativa e persona atribuídaescopo restrito às cooperativas daquele tenant, persona resolvida daquele vínculo
qualquer personatenant sem membership ativa, ou membership sem persona atribuída403 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 organizationId fora do tenant ativo → 403;
  • GET /api/<recurso>/:id de um registro fora do tenant ativo → 404;
  • tenant ativo sem nenhuma cooperativa vinculada → lista vazia (não vira união);
  • vínculo (UserTenant) sem persona atribuí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/tenants e GET /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 de 403; 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:

  1. Verifica o JWT contra o JWKS de KEYCLOAK_AUTH_URL.
  2. Lê o azp (authorized party = client_id que chamou, ex.: app1-tenant-xyz).
  3. Resolve o tenant correspondente àquele client (via keycloakClientId), populando req.service com o contexto de serviço (tenantId, escopos).
  4. 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.

Autenticação | Profile Server API