Catalisa.Building Blocks
Catálogo/Identidade/IAM

IAM

Produção

Identidade, organizações e permissões para todo o catálogo de building blocks

22
Endpoints
6
Entidades
0
Provedores
Global
Escopo
3001
Porta
2026-02
Desde

Você não integra login em cada serviço que contrata. Contrata o IAM uma vez e todos os 32 building blocks passam a saber quem é o usuário, de que empresa ele é e o que ele pode fazer.

Para quem é
  • Fintechs e bancos digitais que operam múltiplas empresas clientes na mesma plataforma
  • Plataformas B2B que vendem para empresas e precisam separar dados por cliente
  • Times de produto que integram vários serviços e não querem replicar autorização em cada um
Substitui
  • Assinatura de um provedor de identidade externo (Auth0, Cognito) para o cenário B2B multi-tenant
  • Tabela de usuários, papéis e permissões reimplementada dentro de cada serviço
  • Camada caseira de "de qual empresa é este registro" espalhada pelo código
O que não é
  • Um provedor de identidade para o usuário final do seu app (login social, SAML — isso é o building block SSO)
  • Um gerenciador de senhas ou cofre de segredos
  • Um sistema de KYC ou verificação de identidade documental
O que dá para fazer

23 endpoints em 6 recursos.

Explorar a API →
01

Resumo executivo

O IAM responde três perguntas para todos os outros building blocks: quem é este usuário, de qual empresa ele é e o que ele tem permissão de fazer. Ele emite o token que carrega essas três respostas, e todo o resto da plataforma confia nesse token em vez de manter cadastro próprio.

Na prática isso significa que quando uma financeira contrata o Commerce e o Payments, ela não cadastra usuário duas vezes nem descobre, três meses depois, que o time de atendimento consegue estornar pagamento porque alguém esqueceu de checar permissão em um endpoint. A permissão PAYMENTS_REFUND_CREATE é a mesma string em todo lugar, e quem não a tem no token recebe 403 antes de a requisição chegar na regra de negócio.

Está em produção desde fevereiro de 2026 e é dependência obrigatória de todos os outros 31 building blocks — nenhum sobe sem ele.

AtributoValor
Identificadoriam
CategoriaIdentidade
EscopoGlobal (não exige organizationId para operar)
Porta (standalone)3001
Path alias@iam
Prefixo HTTP/iam
StatusProdução desde 2026-02
Depende dePostgreSQL, Redis

02

O problema

negócio

O cenário. Uma plataforma B2B vende para empresas, não para pessoas. Cada empresa cliente tem seus próprios usuários, seus próprios dados, e não pode enxergar um byte da empresa vizinha. Conforme a plataforma cresce, ela deixa de ser um serviço e vira dez — e cada um precisa saber quem está chamando.

É aqui que a arquitetura se degrada em silêncio. Cada serviço novo chega com a própria noção de quem-pode-o-quê, e a plataforma passa a ter tantas implementações de autorização quantos serviços tiver.

flowchart TD
  U["Usuário da empresa cliente"] --> A["Serviço A"]
  U --> B["Serviço B"]
  U --> C["Serviço C"]
  A --> AA["Tabela de usuários própria<br/>Checagem de permissão própria"]
  B --> BB["Outra tabela<br/>Outra checagem"]
  C --> CC["Sem checagem<br/>alguém esqueceu"]
  AA --> R["Risco: regra divergente por serviço"]
  BB --> R
  CC --> R

O que trava hoje.

  • Cada serviço reimplementa autorização. O time do serviço A checa permissão de um jeito, o do serviço B de outro, e o terceiro esquece de checar. O bug não aparece em teste, aparece em auditoria.
  • O provedor de identidade externo não conhece o seu domínio. Auth0 sabe que o usuário fez login; não sabe se ele pode cancelar uma assinatura. A regra volta para o seu código, replicada em cada serviço.
  • Multi-tenancy vira convenção, não garantia. "Todo mundo lembra de filtrar por organization_id" é uma frase que funciona até o dia em que alguém não lembra. Vazamento entre clientes é o incidente que não tem pedido de desculpas suficiente.
  • Integração máquina a máquina é gambiarra. Sem M2M de verdade, o parceiro acaba com um usuário de serviço com senha em variável de ambiente e permissão de administrador.
  • O custo por usuário não fecha no B2B. Preço por usuário ativo mensal foi desenhado para aplicativo de consumidor. Numa plataforma B2B com dezenas de milhares de usuários corporativos de baixa frequência, a conta cresce sem relação com o valor entregue.

O custo de não resolver. Autorização espalhada é a origem mais comum de falha de controle de acesso — a categoria "Broken Access Control" lidera o OWASP Top 10 desde 2021 (OWASP Top 10:2021). E o custo não é só o incidente: é cada novo serviço nascendo com mais duas semanas de trabalho só para reimplementar quem-pode-o-quê.


03

Proposta de valor

negócio
AntesDepois
Cada serviço tem sua tabela de usuários e sua lógica de permissãoUm token, um vocabulário de permissões, 32 serviços obedecendo
"Lembrar de filtrar por organização" é disciplina de equipeBuilding block tenant-scoped recusa requisição sem organização no token
Integração de parceiro usa usuário de serviço com senha fixaclient_credentials com escopo, credencial rotacionável, expiração curta
Adicionar um serviço novo custa duas semanas de autorizaçãoO serviço novo declara suas permissões e já nasce protegido

O desenho que sustenta essa tabela é simples: um único ponto emite a credencial, e todo o resto apenas verifica.

flowchart LR
  U["Usuário"] -->|"login"| IAM["IAM"]
  P["Parceiro"] -->|"client_credentials"| IAM
  IAM -->|"JWT: sub, organizationId, permissions"| BBs["32 building blocks"]
  BBs -->|"requirePermission + requireOrganization"| OK["Resposta ou 403"]

Autorização escrita uma vez

As permissões são um enum compartilhado por todos os building blocks. O middleware requirePermission é o mesmo código em todo lugar — não há trinta implementações para auditar, há uma.

O tenant vem do token, nunca do corpo da requisição

O organizationId é claim assinado. Um cliente não consegue pedir dados de outro trocando um campo no JSON, porque o campo não é lido do JSON.

Máquina a máquina de verdade

O parceiro recebe client_id (que é o identificador da própria organização) e client_secret rotacionável, troca por token de vida curta e opera com escopo reduzido ao que precisa.

Custo desacoplado do número de usuários

O IAM vem com a plataforma. Uma empresa cliente com 40 usuários corporativos não custa 40 vezes uma com um usuário.


04

Casos de uso reais

negócio

Caso 1 — Uma financeira separa correspondentes bancários sem duplicar infraestrutura Cenário ilustrativo

Contexto

Financeira de crédito consignado que opera através de 120 correspondentes bancários. Cada correspondente tem de 3 a 40 operadores, e nenhum pode ver proposta de outro.

A dor

A primeira versão da plataforma tinha uma instância por correspondente. Cento e vinte bancos de dados, cento e vinte deploys, e uma migração de schema que levava um fim de semana inteiro. O custo de infraestrutura crescia linearmente com as vendas, o que é exatamente o contrário do que se espera de software.

A solução com o BB

Cada correspondente vira uma Organization do tipo parceiro. Os operadores viram User associados a essa organização por uma Association que carrega o papel. Todo building block tenant-scoped — Customers, Products, Decision Platform — lê o organizationId do token e nunca do corpo. A separação deixa de ser topológica e passa a ser criptográfica.

flowchart LR
  subgraph antes["Antes — separação topológica"]
    C1["Correspondente 1"] --> I1["Instância + banco 1"]
    C2["Correspondente 2"] --> I2["Instância + banco 2"]
    C120["Correspondente 120"] --> I120["Instância + banco 120"]
  end
  subgraph depois["Depois — separação por token"]
    O1["Organization 1"] --> T["Token com organizationId"]
    O2["Organization 2"] --> T
    O120["Organization 120"] --> T
    T --> U["Uma instância<br/>um deploy<br/>uma migração"]
  end
  antes -.->|"migração"| depois
O resultado

Uma instância, um deploy, uma migração. Adicionar um correspondente vira uma chamada POST /iam/api/v1/organizations em vez de um provisionamento de ambiente.

Caso 2 — Um parceiro integra por API sem nunca receber senha de usuário Cenário ilustrativo

Contexto

Marketplace que envia pedidos para o ERP de um cliente através da API da plataforma, sem interface humana no meio.

A dor

A integração anterior usava um usuário de serviço chamado integracao@empresa.com com senha em variável de ambiente do marketplace. A senha nunca expirava, tinha permissão de administrador porque ninguém quis descobrir o conjunto mínimo, e não havia como revogar sem quebrar a integração em produção às três da tarde.

A solução com o BB

O parceiro recebe o client_id (o próprio organizationId) e um client_secret gerado por POST /iam/api/v1/organizations/:id/client-secret. A integração troca as credenciais em POST /iam/oauth/token com grant_type=client_credentials e recebe um token de uma hora, com permissões filtradas pelo scope solicitado. Rotacionar o segredo é uma chamada de API.

sequenceDiagram
  participant Op as Operador da plataforma
  participant IAM
  participant Mk as Marketplace
  participant BB as Building block tenant-scoped
  Op->>IAM: POST /organizations/:id/client-secret
  IAM-->>Op: clientId + clientSecret (exibido uma vez)
  Op->>Mk: entrega credencial
  Mk->>IAM: POST /oauth/token (client_credentials + scope)
  IAM-->>Mk: access_token (1h, escopo reduzido)
  Mk->>BB: Authorization Bearer
  BB-->>Mk: dados apenas da organização do token
O resultado

Credencial com escopo mínimo, expiração curta e rotação sem downtime. E toda emissão de token fica registrada no log de segurança, então "quando essa integração foi usada pela última vez" vira uma consulta em vez de uma suposição.

Caso 3 — Uma auditoria de acesso deixa de ser um projeto de três semanas Cenário ilustrativo

Contexto

Operação regulada precisa responder ao auditor quem tinha acesso a dados de cliente no último trimestre.

A dor

Com autorização espalhada por dez serviços, responder isso significava dez levantamentos diferentes, cada um com um dono, um formato e um grau de confiança distinto. O relatório chegava desatualizado.

A solução com o BB

O acesso é a interseção de três tabelas: Association liga usuário, organização e papel; Role define o papel; OrganizationPermission define o que a organização contratou. A resposta é uma consulta — a consulta de auditoria cruza Association com OrganizationPermission e resolve as permissões do papel pelo mapa ROLE_PERMISSIONS do código (src/iam/constants/permissions.ts), já que Role guarda apenas o nome. O Audit Trail complementa registrando o que foi de fato acessado.

erDiagram
  User ||--o{ Association : "participa"
  Organization ||--o{ Association : "recebe"
  Role ||--o{ Association : "define papel"
  Organization ||--o{ OrganizationPermission : "contratou"
O resultado

A pergunta "quem podia ver o quê" tem uma fonte de verdade, e a resposta é reproduzível.

Caso 4 — Multi-tenancy como fundação, não como remendo Referência de mercado

Contexto

O padrão de banco de dados compartilhado com discriminador de tenant é a arquitetura recomendada pela AWS e pela Microsoft para SaaS B2B, justamente pelo custo por tenant (AWS SaaS Tenant Isolation Strategies).

A dor

No mercado, o mesmo material é explícito no risco: com banco compartilhado, o isolamento passa a depender inteiramente da camada de aplicação. Uma cláusula WHERE esquecida é um vazamento entre clientes.

A solução com o BB

É assim que a Catalisa endereça o risco: o organizationId chega por claim assinado e nunca pelo corpo. O middleware requireOrganization bloqueia antes da regra de negócio. E building blocks que guardam dado sensível — o Data Store é o exemplo — somam Row-Level Security no Postgres como segunda barreira, para que a falha de aplicação não seja suficiente para vazar.

flowchart LR
  R["Requisição com Bearer JWT"] --> M1["authMiddleware<br/>assinatura HS256 válida?"]
  M1 -->|não| E401["401"]
  M1 -->|sim| M2["requireOrganization<br/>claim organizationId presente?"]
  M2 -->|não| E403["403"]
  M2 -->|sim| M3["requirePermission<br/>permissão no token?"]
  M3 -->|não| E403
  M3 -->|sim| N["Regra de negócio"]
  N --> RLS["Row-Level Security no Postgres<br/>segunda barreira, nos BBs sensíveis"]
  RLS --> D["Dados apenas do tenant"]
O resultado

O padrão econômico de multi-tenancy sem aceitar o risco que normalmente vem junto.


05

Mercado e diferenciais

negócio

Panorama. O mercado de identidade se dividiu em três campos. Os provedores hospedados (Auth0, Clerk, Cognito) resolvem autenticação muito bem e cobram por usuário ativo. As soluções auto-hospedadas (Keycloak, FusionAuth) entregam protocolo completo e transferem a operação para você. E, na prática, quase toda plataforma B2B acaba escrevendo uma terceira camada em cima — a autorização de domínio, que nenhum dos dois grupos resolve, porque nenhum deles sabe o que é uma fatura na sua regra de negócio.

O IAM da Catalisa nasce nesse terceiro campo. Ele não tenta ser o melhor autenticador do mercado; ele é o autorizador que já conhece os 32 building blocks.

CritérioCatalisa IAMAuth0Amazon CognitoKeycloak
Modelo de preçoIncluso na plataformaPor usuário ativo mensalPor usuário ativo mensalLicença zero, você opera
Organização como cidadã de 1ª classeSim, no tokenSó nos planos superioresNãoVia realm, um por tenant
Autorização de domínioVocabulário único nos 32 BBsFora do escopoFora do escopoGenérica, você mapeia
M2M com escopoclient_credentials nativoSim, cobrado à parteSimSim
Login social e SAMLNão (é o BB SSO)Sim, extensoParcialSim, extenso
MFANão implementado (ver §15)SimSimSim
Tela de login hospedadaNãoSimSimSim
Operação por sua contaJá vem operadoNãoNãoSim, integral

Nossos diferenciais

  1. A permissão é do domínio, não genérica. BILLING_INVOICES_VOID significa alguma coisa concreta porque o Billing a declara e o middleware a exige. Um IdP externo só conseguiria chegar aqui se você reimplementasse o catálogo inteiro dentro dele — e mantivesse os dois em sincronia para sempre.
  2. O tenant não é opcional em nenhum ponto do caminho. O organizationId é claim assinado, e os building blocks tenant-scoped rejeitam token sem ele. Não é convenção de time, é o código recusando.
  3. A credencial M2M já nasce com o tenant amarrado. O client_id é o identificador da organização. Não existe estado em que uma credencial de integração esteja apontando para o tenant errado, porque ela é o tenant.
  4. Custo desacoplado de usuário ativo. No B2B, o número de usuários corporativos tem pouca relação com o valor entregue. Cobrar por ele penaliza exatamente o cliente que mais cresce.

Quando escolher o concorrente. O IAM ganha quando o problema é autorização de domínio em plataforma B2B multi-tenant — não quando o problema é autenticação de consumidor final. Nos três cenários abaixo, a escolha certa é outra.

flowchart TD
  Q1{"Seu produto é B2C,<br/>com login social e<br/>tela de login pronta?"}
  Q1 -->|sim| A["Auth0 ou Clerk"]
  Q1 -->|não| Q2{"Exige SAML corporativo<br/>ou LDAP?"}
  Q2 -->|sim| K["Keycloak"]
  Q2 -->|não| Q3{"Já vive na AWS, com<br/>volume alto de usuários?"}
  Q3 -->|sim| C["Amazon Cognito"]
  Q3 -->|não| I["Catalisa IAM"]
Se o seu caso éEscolhaPor quê
B2C com login Google/Apple, tela de login pronta, MFA por SMS e recuperação de senha com boa conversãoAuth0 ou ClerkEntregam isso hoje; o IAM não. Nossa camada de identidade federada é um building block separado, o SSO, e o MFA ainda não está implementado
SAML corporativo ou LDAPKeycloakÉ a escolha madura em cobertura de protocolo
Já vive dentro da AWS, volume alto de usuários e tolerância à experiência de desenvolvimentoAmazon CognitoSerá mais barato que qualquer alternativa

06

Modelo de cobrança e ROI

negócio

Unidade de cobrança. O IAM não é cobrado à parte. Ele é infraestrutura obrigatória de todos os outros building blocks, e cobrar por ele seria cobrar duas vezes pela mesma coisa.

O que dispara custo. Número de organizações ativas e volume de emissão de token — ambos com impacto marginal, já que a emissão é uma assinatura HS256 e a verificação não toca o banco.

Comparação de custo — cenário: plataforma B2B com 80 empresas clientes e 4.000 usuários corporativos, dos quais cerca de 2.500 acessam em um mês típico.

Catalisa IAMAuth0 (B2B Essentials)Amazon Cognito
Base de cálculoIncluso na plataformaPor usuário ativo mensalPor usuário ativo mensal, após a faixa gratuita
Ordem de grandeza mensalR$ 0 adicionalAlguns milhares de reaisDezenas de reais
Autorização de domínioInclusaVocê implementaVocê implementa
Custo de engenharia estimado—2 a 4 semanas por serviço novo2 a 4 semanas por serviço novo

Ordens de grandeza para orientar conversa, não proposta comercial. Preços de Auth0 e Cognito variam por região, plano e negociação — consulte as tabelas públicas dos fornecedores na data da sua análise. O custo de engenharia é estimativa interna.

ROI

O retorno não está na linha de licença — o Cognito é mais barato que qualquer coisa. Está na engenharia que não é gasta.

flowchart LR
  A["Building block novo entra no catálogo"] --> B{"Existe IAM?"}
  B -->|não| C["2 a 4 semanas reimplementando<br/>quem-pode-o-quê + risco de errar"]
  B -->|sim| D["Declara as permissões<br/>e já nasce protegido"]
  C --> E["Coleção de serviços<br/>que não conversam"]
  D --> F["Catálogo integrado"]

Cada building block novo que entra no catálogo já nasce autorizado; sem o IAM, cada um custaria de duas a quatro semanas de trabalho só para reimplementar quem-pode-o-quê, com a chance de errar que isso carrega. Com 32 building blocks, essa é a diferença entre um catálogo e uma coleção de serviços que não conversam.


07

Arquitetura

As camadas e o caminho da requisição

flowchart TD
  HTTP["HTTP"] --> HONO

  subgraph HONO["Hono app — basePath('/iam')"]
    R1["/api/v1/users<br/>usersRouter · userAssociationsRouter"]
    R2["/api/v1/organizations<br/>organizationsRouter · membersRouter"]
    R3["/api/v1/roles<br/>rolesRouter"]
    R4["/api/v1/permissions<br/>permissionsRouter"]
    R5["/api/v1/associations<br/>associationsRouter"]
    R6["/oauth<br/>oauthRouter — token · revoke"]
    R7["/health<br/>sonda com banco, cripto e JWT"]
  end

  HONO -->|"Zod parse → ResultAsync&lt;T, AppError&gt;"| SVC

  subgraph SVC["services/"]
    S1["AuthService — login, emissão de par de tokens"]
    S2["UserService — ciclo de vida do usuário"]
    S3["OrganizationService — organizações e client_secret"]
    S4["AssociationService — usuário ↔ organização ↔ papel"]
    S5["RoleService — catálogo de papéis e permissões"]
    S6["OrganizationPermissionService — o que a org contratou"]
    S7["RefreshTokenService — rotação e revogação"]
  end

  SVC --> REPO["repositories/ (Prisma)"]
  REPO --> PG[("PostgreSQL — schema iam")]

O token sai daqui e é verificado em todo lugar

O token emitido pelo IAM é consumido por todos os outros building blocks, e a verificação é o mesmo código nos 32:

flowchart LR
  C["Cliente"] -->|"Bearer JWT"| MW
  subgraph MW["Middlewares — idênticos nos 32 BBs"]
    direction TB
    A["authMiddleware<br/>verifica assinatura HS256"] --> B["requirePermission(P)<br/>P ∈ token.permissions?"]
    B --> D["requireOrganization<br/>token.organizationId presente?"]
  end
  MW --> H["Handler da regra de negócio"]

Decisões não óbvias.

  • HS256 com segredo compartilhado, e não RS256. Todos os building blocks são nossos e rodam na nossa infraestrutura, então a chave pública não traria benefício e a rotação de par de chaves traria custo operacional. O trade-off aceito é que qualquer serviço com o segredo pode emitir token, não só verificar — o que é aceitável enquanto todos os consumidores forem internos. No dia em que um terceiro precisar verificar token sem poder emitir, isso vira RS256.
  • JWT_SECRET com mínimo de 44 caracteres, validado no construtor. É o comprimento de uma chave de 256 bits em base64. O serviço não sobe com segredo fraco; falhar no boot é melhor que rodar inseguro.
  • Preflight de JWT no startup. O JwtService.preflight() assina e verifica um token descartável antes de o serviço aceitar tráfego. Um segredo malformado quebra no boot, não na primeira requisição de um cliente.
  • Permissões dentro do token, não consultadas por requisição. A verificação não toca o banco, o que mantém a latência do middleware perto de zero. O trade-off é que revogar permissão só surte efeito na renovação do token — por isso o access token dura uma hora por padrão.
  • isRoot como claim explícito. Operador de plataforma precisa de acesso cross-organização. Isso é um claim booleano e nomeado, em vez de um caso especial escondido em cada rota, justamente para que seja auditável.
  • Rate limit nas rotas de autenticação. POST /users/login e POST /oauth/token passam por authRateLimitMiddleware, e falhas geram evento em logSecurityEvent. Força bruta é ruidosa e limitada por padrão.

Monolito vs. standalone

Em monolito, os outros building blocks resolvem o IAM pelo container TypeDI, com chamada direta. Em standalone — o modo usado em produção — o IAM roda na porta 3001 e os demais o alcançam por HTTP via ModuleClient.

MonolitoStandalone (produção)
Como os BBs alcançam o IAMContainer TypeDI, chamada direta em processoHTTP via ModuleClient, porta 3001
Emissão de tokenNo mesmo processoRequisição de rede ao IAM
Verificação de tokenLocal, sem redeLocal, sem rede

A verificação não precisa de rede em nenhum dos dois modos: qualquer serviço com o JWT_SECRET valida a assinatura localmente.


08

Conceitos e modelo de dados

Glossário

TermoSignifica
OrganizationUma empresa dentro da plataforma. É a unidade de isolamento: todo dado tenant-scoped pertence a exatamente uma.
UserUma pessoa. Existe globalmente, com e-mail único, e pode pertencer a várias organizações.
AssociationO vínculo entre usuário, organização e papel. É o que responde "esta pessoa, nesta empresa, é o quê".
RolePapel nomeado (ADMIN, VIEWER, ...). Agrupa permissões.
PermissionString do vocabulário compartilhado, no formato MODULO_RECURSO_ACAO — por exemplo IAM_USERS_CREATE.
OrganizationPermissionO que a organização contratou. Limite superior: um usuário nunca recebe permissão que a organização dele não tem.
Client secretSegredo da organização para autenticação M2M. Guardado como hash, exibido só na criação.
isRootMarca de operador de plataforma. Libera acesso cross-organização.

Modelo de dados — schema iam no PostgreSQL.

Modelo PrismaTabelaPropósitoCampos-chave
Useriam.usersPessoa autenticávelemail (único), passwordHash, status, isRoot, mfaEnabled, deletedAt
Organizationiam.organizationsEmpresa cliente, parceiro ou donoslug (único), type, status, clientSecretHash, metadata
Associationiam.associationsVínculo usuário ↔ organização ↔ papelChave primária composta (userId, organizationId, roleId)
Roleiam.rolesCatálogo de papéisname (único)
OrganizationPermissioniam.organization_permissionsPermissões contratadas pela organizaçãoÚnico (organizationId, permission)
RefreshTokeniam.refresh_tokensRefresh tokens emitidostoken (único), expiresAt

Enumerações

EnumValores
UserStatusativo · inativo · pendente · bloqueado
OrganizationTypecliente · parceiro · dono
OrganizationStatusativo · inativo · suspenso

Relacionamentos

erDiagram
  User ||--o{ Association : "pertence a"
  Organization ||--o{ Association : "tem membros"
  Role ||--o{ Association : "papel do vínculo"
  Organization ||--o{ OrganizationPermission : "contratou"
  User ||--o{ RefreshToken : "possui"

Ciclo de vida do usuário

stateDiagram-v2
  [*] --> pendente: criação (POST /users)
  pendente --> ativo: ativação (PATCH status)
  ativo --> bloqueado: suspensão (PATCH status)
  ativo --> inativo: desativação (PATCH status)
  ativo --> [*]: exclusão lógica (deletedAt)
  inativo --> [*]: exclusão lógica (deletedAt)
  bloqueado --> [*]: exclusão lógica (deletedAt)

Atenção. O usuário nasce pendente e só o status ativo autentica — o AuthService recusa qualquer outro. A exclusão é lógica: deletedAt é preenchido e a linha é preservada para auditoria.

Ciclo de vida da organização

stateDiagram-v2
  [*] --> ativo: criação (POST /organizations, padrão ativo)
  ativo --> suspenso: suspensão (PATCH status)
  suspenso --> ativo: reativação (PATCH status)
  ativo --> inativo: desativação (PATCH status)
  inativo --> ativo: reativação (PATCH status)
  ativo --> [*]: exclusão lógica (deletedAt)
  inativo --> [*]: exclusão lógica (deletedAt)
  suspenso --> [*]: exclusão lógica (deletedAt)

Como uma permissão chega ao token

flowchart TD
  A["Permissões da organização<br/>(OrganizationPermission)"] --> X{"interseção"}
  B["Permissões do papel<br/>(Role → ROLE_PERMISSIONS)"] --> X
  X --> T["token.permissions[]<br/>o que o usuário de fato pode fazer"]

Atenção. A organização é o teto. Papel de ADMIN numa organização que não contratou o Billing não concede nenhuma permissão de Billing.


09

Referência da API

Prefixo: /iam. Em standalone, a base é https://iam.stg.catalisa.app.

Usuários — /iam/api/v1/users

MétodoRotaDescriçãoPermissão
POST/iam/api/v1/usersCria usuárioIAM_USERS_CREATE
POST/iam/api/v1/users/loginAutentica e emite par de tokensPública (com rate limit)
GET/iam/api/v1/usersLista usuários, paginadoIAM_USERS_READ
GET/iam/api/v1/users/:userIdBusca usuárioIAM_USERS_READ
PATCH/iam/api/v1/users/:userIdAtualiza usuárioIAM_USERS_UPDATE
DELETE/iam/api/v1/users/:userIdExclusão lógicaIAM_USERS_DELETE
GET/iam/api/v1/users/:userId/associationsOrganizações e papéis do usuárioIAM_ASSOCIATIONS_READ

Organizações — /iam/api/v1/organizations

MétodoRotaDescriçãoPermissão
POST/iam/api/v1/organizationsCria organizaçãoIAM_ORGANIZATIONS_CREATE
GET/iam/api/v1/organizationsLista organizaçõesIAM_ORGANIZATIONS_READ
GET/iam/api/v1/organizations/:organizationIdBusca organizaçãoIAM_ORGANIZATIONS_READ
PATCH/iam/api/v1/organizations/:organizationIdAtualiza organizaçãoIAM_ORGANIZATIONS_UPDATE
DELETE/iam/api/v1/organizations/:organizationIdExclusão lógicaIAM_ORGANIZATIONS_DELETE
POST/iam/api/v1/organizations/:organizationId/client-secretGera ou rotaciona o segredo M2MIAM_ORGANIZATIONS_UPDATE
GET/iam/api/v1/organizations/:organizationId/membersLista membrosIAM_ASSOCIATIONS_READ

Papéis e permissões

MétodoRotaDescriçãoPermissão
GET/iam/api/v1/rolesLista papéisIAM_ROLES_READ
GET/iam/api/v1/roles/:roleName/permissionsPermissões de um papelIAM_ROLES_READ
GET/iam/api/v1/permissionsPermissões da organização atualIAM_ROLES_READ
GET/iam/api/v1/permissions/allVocabulário completo da plataformaIAM_ROLES_READ

Associações — /iam/api/v1/associations

MétodoRotaDescriçãoPermissão
POST/iam/api/v1/associationsVincula usuário a organização com papelIAM_ASSOCIATIONS_CREATE
DELETE/iam/api/v1/associationsRemove o vínculoIAM_ASSOCIATIONS_DELETE

OAuth 2.0 — /iam/oauth

MétodoRotaDescriçãoPermissão
POST/iam/oauth/tokenclient_credentials ou refresh_tokenPública (com rate limit)
POST/iam/oauth/revokeRevoga um refresh tokenPública

Saúde

MétodoRotaDescrição
GET/iam/healthSonda banco, criptografia e round-trip de JWT. 503 se algum falhar.

Endpoints principais em detalhe

Os dois endpoints abaixo são os que um integrador usa primeiro. O caminho é o mesmo nos dois: credencial entra, token sai.

sequenceDiagram
  participant Cli as Cliente
  participant RL as authRateLimitMiddleware
  participant Auth as AuthService
  participant DB as PostgreSQL
  participant Log as logSecurityEvent
  Cli->>RL: POST /users/login ou /oauth/token
  RL-->>Cli: 429 se o limite estourou
  RL->>Auth: credenciais
  Auth->>DB: busca usuário ou organização
  DB-->>Auth: registro + hash
  Auth->>Auth: verifica hash e resolve permissões
  Auth->>Log: LOGIN_SUCCESS / LOGIN_FAILURE / OAUTH_TOKEN_FAILURE
  Auth-->>Cli: 200 com token, ou 401

POST /iam/api/v1/users/login

Autentica por e-mail e senha. Rota pública, protegida por rate limit; sucesso e falha vão para o log de segurança.

Request

json
{
  "email": "admin@catalisa.app",
  "password": "senha-do-usuario",
  "organizationId": "2194956a-2d0a-4203-89a3-933319d80fa1"
}
{
  "email": "admin@catalisa.app",
  "password": "senha-do-usuario",
  "organizationId": "2194956a-2d0a-4203-89a3-933319d80fa1"
}
CampoTipoObrigatórioDescrição
emailstring (e-mail)SimE-mail do usuário
passwordstring (máx. 128)SimSenha em texto puro, sobre TLS
organizationIdstringNãoAceita UUID ou slug. Omitido, o serviço resolve a organização do usuário. Necessário quando o usuário pertence a mais de uma.
mfaCodestringNãoAceito pelo schema, ainda não verificado — ver §15

Resposta 200

json
{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": "b1000000-0000-0000-0000-000000000001",
    "email": "admin@catalisa.app",
    "status": "ativo"
  }
}
{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": "b1000000-0000-0000-0000-000000000001",
    "email": "admin@catalisa.app",
    "status": "ativo"
  }
}

Claims do access token

ClaimDescrição
subID do usuário
emailE-mail
typeaccess ou refresh
organizationIdOrganização do contexto — o tenant efetivo da requisição
permissionsLista de permissões já resolvida
isRoottrue só para operador de plataforma
expExpiração — JWT_EXPIRES_IN, padrão 3600s

Erros

StatusQuando
400Corpo não passa no schema Zod
401Credenciais inválidas, usuário não ativo, ou sem acesso à organização informada
429Rate limit de autenticação estourado

POST /iam/oauth/token

Endpoint OAuth 2.0. Suporta client_credentials (M2M) e refresh_token.

Request — client_credentials

json
{
  "grant_type": "client_credentials",
  "client_id": "2194956a-2d0a-4203-89a3-933319d80fa1",
  "client_secret": "cs_live_...",
  "scope": "CUSTOMERS_READ CUSTOMERS_CREATE"
}
{
  "grant_type": "client_credentials",
  "client_id": "2194956a-2d0a-4203-89a3-933319d80fa1",
  "client_secret": "cs_live_...",
  "scope": "CUSTOMERS_READ CUSTOMERS_CREATE"
}
CampoTipoObrigatórioDescrição
grant_typeclient_credentials | refresh_tokenSimFluxo
client_idstringEm client_credentialsÉ o organizationId
client_secretstringEm client_credentialsSegredo gerado na rota de client-secret
scopestringNãoPermissões separadas por espaço. Omitido, o token recebe todas as permissões da organização.
refresh_tokenstringEm refresh_tokenO refresh token a trocar

O scope só reduz: pedir uma permissão que a organização não contratou não a concede. O filtro está em filterScopedPermissions.

Resposta 200

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "CUSTOMERS_READ CUSTOMERS_CREATE"
}
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "CUSTOMERS_READ CUSTOMERS_CREATE"
}

Atenção. No fluxo refresh_token a resposta traz também um refresh_token novo: o token apresentado é consumido na hora (uso único) e substituído por outro. Guarde o novo valor — reapresentar o antigo devolve 400 invalid_grant e registra REFRESH_TOKEN_REPLAY_ATTEMPT no log de segurança. O expires_in vem de OAUTH_ACCESS_TOKEN_TTL, e não de JWT_EXPIRES_IN.

Erros — formato OAuth 2.0, não o envelope padrão da plataforma.

StatuserrorQuando
400unsupported_grant_typegrant_type fora dos suportados
401invalid_clientclient_id/client_secret ausente ou inválido
400invalid_grantRefresh token ausente, inválido, expirado, revogado ou já consumido
500server_errorFalha ao emitir o novo refresh token na rotação
429—Rate limit estourado

O serviço não distingue organização inexistente de segredo errado — ambos retornam invalid_client. É proposital: distinguir permitiria enumerar organizações.


POST /iam/api/v1/organizations/:organizationId/client-secret

Gera ou rotaciona o segredo M2M. O valor é retornado uma única vez; o banco guarda apenas o hash.

Resposta 200

json
{
  "clientId": "2194956a-2d0a-4203-89a3-933319d80fa1",
  "clientSecret": "cs_live_8f3a...",
  "warning": "Guarde este segredo agora. Ele não será exibido novamente."
}
{
  "clientId": "2194956a-2d0a-4203-89a3-933319d80fa1",
  "clientSecret": "cs_live_8f3a...",
  "warning": "Guarde este segredo agora. Ele não será exibido novamente."
}

Rotacionar invalida o segredo anterior de imediato — coordene com o parceiro antes de chamar.


10

Início rápido

Do zero a uma chamada autenticada em um building block tenant-scoped, em quatro passos.

sequenceDiagram
  autonumber
  participant Você
  participant IAM
  participant Cust as Customers
  Você->>IAM: POST /users/login (e-mail, senha, organizationId)
  IAM-->>Você: accessToken + refreshToken
  Você->>Você: decodifica o payload e confere organizationId
  Você->>Cust: GET /persons com Bearer
  Cust-->>Você: só os clientes da sua organização
  Você->>Cust: GET /persons com token inválido
  Cust-->>Você: 401

1. Autenticar

bash
TOKEN=$(curl -s -X POST https://iam.stg.catalisa.app/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@catalisa.app",
    "password": "password123",
    "organizationId": "2194956a-2d0a-4203-89a3-933319d80fa1"
  }' | jq -r .accessToken)

echo "${TOKEN:0:32}..."
TOKEN=$(curl -s -X POST https://iam.stg.catalisa.app/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@catalisa.app",
    "password": "password123",
    "organizationId": "2194956a-2d0a-4203-89a3-933319d80fa1"
  }' | jq -r .accessToken)

echo "${TOKEN:0:32}..."

Resposta esperada — o começo de um JWT, prova de que a variável foi preenchida:

texto
eyJhbGciOiJIUzI1NiIsInR5cCI6Ik...
eyJhbGciOiJIUzI1NiIsInR5cCI6Ik...

2. Inspecionar o que o token carrega

bash
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq
json
{
  "sub": "b1000000-0000-0000-0000-000000000001",
  "email": "admin@catalisa.app",
  "type": "access",
  "organizationId": "2194956a-2d0a-4203-89a3-933319d80fa1",
  "permissions": ["IAM_USERS_READ", "CUSTOMERS_READ", "..."],
  "exp": 1755400000
}
{
  "sub": "b1000000-0000-0000-0000-000000000001",
  "email": "admin@catalisa.app",
  "type": "access",
  "organizationId": "2194956a-2d0a-4203-89a3-933319d80fa1",
  "permissions": ["IAM_USERS_READ", "CUSTOMERS_READ", "..."],
  "exp": 1755400000
}

O organizationId aqui é o tenant efetivo. Todo building block tenant-scoped vai ler daqui — nunca do corpo da sua requisição.

3. Usar o token em outro building block

bash
curl -s https://customers.stg.catalisa.app/customers/api/v1/persons \
  -H "Authorization: Bearer $TOKEN" | jq '.data | length'
curl -s https://customers.stg.catalisa.app/customers/api/v1/persons \
  -H "Authorization: Bearer $TOKEN" | jq '.data | length'

Resposta esperada — a contagem de clientes visíveis para a sua organização:

texto
12
12

Nenhum organizationId na chamada, e ainda assim só vêm os clientes da sua organização.

4. Confirmar que o isolamento é real

bash
curl -s -o /dev/null -w "%{http_code}\n" \
  https://customers.stg.catalisa.app/customers/api/v1/persons \
  -H "Authorization: Bearer token-invalido"
curl -s -o /dev/null -w "%{http_code}\n" \
  https://customers.stg.catalisa.app/customers/api/v1/persons \
  -H "Authorization: Bearer token-invalido"

Retorna 401. Não há caminho em que a ausência de token válido resulte em dado.

Credenciais de staging. Nunca use credencial de produção em documentação ou script de exemplo — ver AMBIENTES.md.


11

Receitas

Criar uma organização e o primeiro administrador dela

Objetivo. Provisionar um cliente novo do zero: organização, usuário administrador e o vínculo entre os dois. Exige um token com permissões de IAM.

flowchart LR
  A["1. POST /organizations"] --> B["2. POST /users"]
  B --> C["3. GET /roles<br/>descobrir o id de ADMIN"]
  C --> D["4. POST /associations<br/>usuário + organização + papel"]
  D --> E["5. PATCH /users/:id<br/>status ativo"]
  E --> F["Administrador consegue autenticar"]

1. Criar a organização

bash
ORG=$(curl -s -X POST https://iam.stg.catalisa.app/iam/api/v1/organizations \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Financeira Exemplo","slug":"financeira-exemplo","type":"cliente"}')
ORG_ID=$(echo "$ORG" | jq -r '.data.id')
echo "$ORG_ID"
ORG=$(curl -s -X POST https://iam.stg.catalisa.app/iam/api/v1/organizations \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Financeira Exemplo","slug":"financeira-exemplo","type":"cliente"}')
ORG_ID=$(echo "$ORG" | jq -r '.data.id')
echo "$ORG_ID"

Resposta esperada — o UUID da organização recém-criada:

texto
3f6c1a02-7f4b-4c2e-9a11-0d6f9b3f1c77
3f6c1a02-7f4b-4c2e-9a11-0d6f9b3f1c77

2. Criar o usuário

bash
USER=$(curl -s -X POST https://iam.stg.catalisa.app/iam/api/v1/users \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"email":"admin@financeira-exemplo.com.br","password":"UmaSenhaForte!2026"}')
USER_ID=$(echo "$USER" | jq -r '.data.id')
echo "$USER" | jq '.data | {id, email, status}'
USER=$(curl -s -X POST https://iam.stg.catalisa.app/iam/api/v1/users \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"email":"admin@financeira-exemplo.com.br","password":"UmaSenhaForte!2026"}')
USER_ID=$(echo "$USER" | jq -r '.data.id')
echo "$USER" | jq '.data | {id, email, status}'

Resposta esperada — repare no status, que nasce pendente:

json
{
  "id": "8a2f0e11-4d3b-4f18-8f0a-3b6b2c9e4d55",
  "email": "admin@financeira-exemplo.com.br",
  "status": "pendente"
}
{
  "id": "8a2f0e11-4d3b-4f18-8f0a-3b6b2c9e4d55",
  "email": "admin@financeira-exemplo.com.br",
  "status": "pendente"
}

3. Descobrir o papel

bash
ROLE_ID=$(curl -s https://iam.stg.catalisa.app/iam/api/v1/roles \
  -H "Authorization: Bearer $TOKEN" | jq -r '.data[] | select(.name=="ADMIN") | .id')
echo "$ROLE_ID"
ROLE_ID=$(curl -s https://iam.stg.catalisa.app/iam/api/v1/roles \
  -H "Authorization: Bearer $TOKEN" | jq -r '.data[] | select(.name=="ADMIN") | .id')
echo "$ROLE_ID"

Resposta esperada — Role.id é inteiro, não UUID:

texto
1
1

4. Associar usuário + organização + papel

bash
curl -s -X POST https://iam.stg.catalisa.app/iam/api/v1/associations \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"userId\":\"$USER_ID\",\"organizationId\":\"$ORG_ID\",\"roleId\":$ROLE_ID}" | jq
curl -s -X POST https://iam.stg.catalisa.app/iam/api/v1/associations \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"userId\":\"$USER_ID\",\"organizationId\":\"$ORG_ID\",\"roleId\":$ROLE_ID}" | jq

Resposta esperada — o vínculo criado, com a chave composta dos três identificadores.

5. Ativar o usuário

bash
curl -s -X PATCH "https://iam.stg.catalisa.app/iam/api/v1/users/$USER_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"status":"ativo"}' | jq '.data.status'
curl -s -X PATCH "https://iam.stg.catalisa.app/iam/api/v1/users/$USER_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"status":"ativo"}' | jq '.data.status'

Resposta esperada:

texto
"ativo"
"ativo"

Armadilhas.

  • O usuário nasce pendente e não autentica até virar ativo — faça o PATCH de status ou o login falha com 401 e ninguém entende por quê.
  • Sem a associação do passo 4, o login retorna 401 mesmo com senha correta: usuário sem organização não tem contexto.
  • A organização precisa ter as permissões contratadas em OrganizationPermission. Papel ADMIN numa organização sem permissões de Billing não concede nenhuma permissão de Billing — a organização é o teto.
  • O slug é único globalmente. Colisão retorna 409.

Entregar credencial M2M a um parceiro

Objetivo. Dar a um parceiro acesso programático com escopo mínimo, sem senha de usuário no meio.

sequenceDiagram
  participant Você
  participant IAM
  participant Parc as Parceiro
  Você->>IAM: POST /organizations/:id/client-secret
  IAM-->>Você: clientId + clientSecret (uma única exibição)
  Você->>Parc: entrega por canal seguro (SOPS)
  Parc->>IAM: POST /oauth/token com scope reduzido
  IAM-->>Parc: access_token de 1 hora

1. Gerar o segredo

bash
curl -s -X POST \
  "https://iam.stg.catalisa.app/iam/api/v1/organizations/$ORG_ID/client-secret" \
  -H "Authorization: Bearer $TOKEN" | jq
curl -s -X POST \
  "https://iam.stg.catalisa.app/iam/api/v1/organizations/$ORG_ID/client-secret" \
  -H "Authorization: Bearer $TOKEN" | jq

Resposta esperada — o clientSecret só aparece aqui, nunca mais:

json
{
  "clientId": "3f6c1a02-7f4b-4c2e-9a11-0d6f9b3f1c77",
  "clientSecret": "cs_live_8f3a...",
  "warning": "Guarde este segredo agora. Ele não será exibido novamente."
}
{
  "clientId": "3f6c1a02-7f4b-4c2e-9a11-0d6f9b3f1c77",
  "clientSecret": "cs_live_8f3a...",
  "warning": "Guarde este segredo agora. Ele não será exibido novamente."
}

2. O parceiro troca a credencial por um token

O parceiro usa assim:

bash
curl -s -X POST https://iam.stg.catalisa.app/iam/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "'"$ORG_ID"'",
    "client_secret": "cs_live_...",
    "scope": "CUSTOMERS_READ"
  }' | jq -r .access_token
curl -s -X POST https://iam.stg.catalisa.app/iam/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "'"$ORG_ID"'",
    "client_secret": "cs_live_...",
    "scope": "CUSTOMERS_READ"
  }' | jq -r .access_token

Armadilhas.

  • O segredo aparece uma vez. Perdeu, rotaciona — não tem recuperação.
  • Rotacionar derruba a integração do parceiro na hora. Combine a janela antes.
  • Sempre passe scope. Sem ele o token recebe todas as permissões da organização, o que é o oposto de privilégio mínimo.
  • Guarde o segredo com SOPS, nunca em .env versionado — ver SECRETS-SOPS-REFERENCE.md.

Renovar o access token sem novo login

Objetivo. Manter a sessão viva por até sete dias sem pedir senha de novo.

sequenceDiagram
  participant App
  participant IAM
  App->>IAM: POST /oauth/token (grant_type refresh_token)
  IAM->>IAM: valida e rotaciona o refresh token
  IAM-->>App: novo access_token (1h)
  Note over App: renove antes de expirar,<br/>não depois de receber 401
bash
curl -s -X POST https://iam.stg.catalisa.app/iam/oauth/token \
  -H "Content-Type: application/json" \
  -d '{"grant_type":"refresh_token","refresh_token":"'"$REFRESH"'"}' | jq
curl -s -X POST https://iam.stg.catalisa.app/iam/oauth/token \
  -H "Content-Type: application/json" \
  -d '{"grant_type":"refresh_token","refresh_token":"'"$REFRESH"'"}' | jq

Resposta esperada — repare que vem um refresh token novo: o antigo foi consumido e não serve mais.

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "scope": null
}
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "scope": null
}

Armadilhas.

  • O refresh token vive JWT_REFRESH_EXPIRES_IN (padrão 7 dias).
  • Renove antes de o access token expirar, não depois de a chamada falhar — tratar 401 como gatilho de renovação gera tempestade de renovação quando o serviço tem concorrência.
  • O refresh token é de uso único e rotaciona a cada troca. Guarde o novo valor devolvido na resposta; reapresentar o antigo devolve 400 invalid_grant e gera um evento REFRESH_TOKEN_REPLAY_ATTEMPT no log de segurança.

Descobrir por que uma requisição voltou 403

Objetivo. Isolar, em quatro perguntas, qual das camadas negou a requisição. Siga na ordem — cada pergunta elimina uma causa.

flowchart TD
  Q1{"A permissão existe<br/>no vocabulário?"}
  Q1 -->|não| F1["Erro de grafia<br/>confira em /permissions/all"]
  Q1 -->|sim| Q2{"A organização contratou?"}
  Q2 -->|não| F2["Falta OrganizationPermission<br/>a organização é o teto"]
  Q2 -->|sim| Q3{"O papel concede?"}
  Q3 -->|não| F3["Troque o papel da Association"]
  Q3 -->|sim| Q4{"O token foi emitido<br/>depois da mudança?"}
  Q4 -->|não| F4["Causa mais comum:<br/>reemita o token"]
  Q4 -->|sim| F5["Investigue o requirePermission da rota"]

1. O que o token realmente carrega

bash
echo "$TOKEN" | cut -d. -f2 | base64 -d | jq '.permissions, .organizationId'
echo "$TOKEN" | cut -d. -f2 | base64 -d | jq '.permissions, .organizationId'

Resposta esperada — a lista de permissões e o tenant efetivo do token:

json
["IAM_USERS_READ", "CUSTOMERS_READ"]
"2194956a-2d0a-4203-89a3-933319d80fa1"
["IAM_USERS_READ", "CUSTOMERS_READ"]
"2194956a-2d0a-4203-89a3-933319d80fa1"

2. O que a organização contratou

bash
curl -s https://iam.stg.catalisa.app/iam/api/v1/permissions \
  -H "Authorization: Bearer $TOKEN" | jq
curl -s https://iam.stg.catalisa.app/iam/api/v1/permissions \
  -H "Authorization: Bearer $TOKEN" | jq

Se a permissão não aparecer aqui, nenhum papel vai concedê-la — a organização é o teto.

3. O vocabulário completo, para conferir a grafia exata

bash
curl -s https://iam.stg.catalisa.app/iam/api/v1/permissions/all \
  -H "Authorization: Bearer $TOKEN" | jq
curl -s https://iam.stg.catalisa.app/iam/api/v1/permissions/all \
  -H "Authorization: Bearer $TOKEN" | jq

Armadilhas. A quarta pergunta do diagrama é a causa mais comum: permissões são resolvidas na emissão do token, então mudança de papel ou de contrato só vale no próximo token — até uma hora depois, com o padrão de JWT_EXPIRES_IN.


12

Integração com outros building blocks

Building blockComo se relacionaObrigatório
Todos os 31 demaisVerificam o token emitido aqui via authMiddlewareSim
API KeysAlternativa ao JWT para acesso programático de longa duraçãoNão
SSOIdentidade federada (login social, provedores externos) apoiada no IAMNão
Audit TrailRegistra o que cada identidade fez; o IAM define quem ela éNão
Webhooks EngineEscopa entrega de eventos por organizaçãoNão
flowchart TD
  U["Usuário"] -->|"login"| IAM
  P["Parceiro"] -->|"client_credentials"| IAM
  IAM["IAM<br/>emite access + refresh token"] -->|"Bearer JWT"| C["Customers"]
  IAM -->|"Bearer JWT"| PY["Payments"]
  IAM -->|"Bearer JWT"| CM["Commerce"]
  IAM -->|"Bearer JWT"| B["Billing"]
  IAM -->|"Bearer JWT"| O["...os outros 27"]
  C --> N["Todos leem organizationId do token,<br/>nunca do corpo"]
  PY --> N
  CM --> N
  B --> N
  O --> N

Este diagrama é o argumento comercial da Catalisa em uma imagem: a autorização é resolvida uma vez, na borda, e vale para o catálogo inteiro. Um provedor de identidade externo pararia na primeira seta.


13

Configuração e operação

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
JWT_SECRETSegredo HS256. Mínimo 44 caracteres — o serviço não sobe abaixo disso. Gere com openssl rand -base64 32.Sim—
JWT_EXPIRES_INVida do access token, em segundosNão3600
JWT_REFRESH_EXPIRES_INVida do refresh token, em segundosNão604800
OAUTH_ACCESS_TOKEN_TTLexpires_in devolvido por /oauth/token, em segundosNão3600
DATABASE_URLPostgreSQLSim—
REDIS_URLRedis, usado no rate limitSim—
DEPLOYMENT_MODEmonolith ou standaloneNãomonolith

Dependências de infraestrutura

DependênciaPara quê
PostgreSQLSchema iam — usuários, organizações, associações, refresh tokens
RedisContadores de rate limit das rotas de autenticação

Limites

LimiteValor
Comprimento da senha no login128 caracteres
Comprimento mínimo do JWT_SECRET44 caracteres
Rate limit de autenticaçãoDefinido em authRateLimitMiddleware
Vida do access token1 hora (padrão)
Vida do refresh token7 dias (padrão)

Catálogo de erros

StatusCódigoSignificaO que fazer
400VALIDATIONCorpo reprovado no ZodConfira campos e tipos contra §9
401UNAUTHORIZEDToken ausente, inválido ou expiradoRenove pelo refresh token
401invalid_clientCredencial M2M inválida (ou organização inexistente)Confira client_id e rotacione o segredo
403FORBIDDENToken válido, permissão faltandoSiga a receita "por que voltou 403" em §11
403—Building block tenant-scoped sem organizationId no tokenAutentique informando a organização
404NOT_FOUNDRecurso inexistente ou excluído logicamenteConfira o ID
409CONFLICTE-mail ou slug já em usoEscolha outro
429—Rate limit de autenticaçãoAplique recuo exponencial
503—/health reprovou banco, cripto ou JWTVerifique Postgres e JWT_SECRET

Observabilidade.

  • GET /iam/health valida conectividade com o banco, o subsistema de senha e um round-trip completo de assinar/verificar JWT. Responde 503 se qualquer um falhar — é a sonda certa para orquestrador.
  • logSecurityEvent registra LOGIN_SUCCESS, LOGIN_FAILURE e OAUTH_TOKEN_FAILURE com IP, user-agent e motivo. É a base para detectar força bruta e credencial M2M vazada.
  • O JwtService.preflight() roda no startup e derruba o boot com segredo malformado.

14

Segurança e compliance

Isolamento entre tenants

O organizationId é claim assinado no JWT. Building blocks tenant-scoped aplicam requireOrganization, que devolve 403 quando o claim está ausente. Building blocks com dado sensível somam Row-Level Security no Postgres como segunda camada, para que uma falha de aplicação não baste para vazar.

flowchart LR
  T["JWT assinado<br/>organizationId"] --> M["requireOrganization"]
  Body["Corpo da requisição"] -.->|"nunca é lido<br/>como fonte de tenant"| X["✕"]
  M --> A["Regra de negócio"]
  A --> RLS["Row-Level Security<br/>nos BBs com dado sensível"]

Atenção. Nenhuma rota lê organizationId do corpo da requisição — essa é a invariante que sustenta o isolamento, e qualquer código novo que a viole é bug de segurança, não escolha de estilo.

Credenciais em repouso

O queComo é guardadoRegras
SenhasHash pelo PasswordService compartilhadoNunca retornadas, nunca logadas. O login limita a senha a 128 caracteres para evitar abuso de custo de hashing
Segredos M2MHash em clientSecretHashExibidos uma única vez, rotacionáveis por API. A verificação não distingue organização inexistente de segredo errado — deliberadamente, para não permitir enumeração

Tokens

HS256, expiração curta no access token, refresh revogável. JWT_SECRET abaixo de 256 bits impede o boot. Padrões fracos comuns geram alerta em produção.

Superfície de ataque de autenticação

Login e emissão de token passam por rate limit e vão para o log de segurança com IP e user-agent.

Exclusão lógica

Usuários e organizações usam deletedAt. O histórico sobrevive à remoção, que é o que auditoria e obrigação de retenção exigem.

LGPD

O IAM guarda e-mail e hash de senha — e-mail é dado pessoal. Exclusão é lógica por padrão; atender a pedido de eliminação exige processo explícito de expurgo, que hoje não é automatizado (ver §15).


15

Limitações conhecidas

LimitaçãoImpactoSituação
MFA não implementadoUser.mfaEnabled, User.mfaSecret e o campo mfaCode no login existem no schema, mas o AuthService não verifica segundo fator. Ativar a flag não protege nada.Roadmap — não anuncie MFA para cliente
Sem login social ou SAMLAutenticação é e-mail e senha ou M2MPor design — é o building block SSO
Sem tela de login hospedadaO cliente constrói a própria interfacePor design
Permissões resolvidas na emissãoMudança de papel só vale no próximo token (até 1h)Por design — o trade-off que mantém a verificação sem tocar o banco
Sem recuperação de senha por e-mailReset é PATCH administrativoRoadmap
Sem expurgo automatizado para LGPDExclusão é lógica; eliminação definitiva é manualRoadmap
HS256 com segredo compartilhadoQualquer serviço com o segredo pode emitir token, não só verificarPor design enquanto todos os consumidores forem internos
Sem cadastro self-serviceOrganização e usuário são criados por quem já tem permissão de IAMEspecificado, não implementado — ver a página de cadastro em docs.catalisa.app

16

Perguntas frequentes

Preciso do IAM se já uso Auth0 na minha empresa?

Sim, mas os papéis são diferentes. O Auth0 continua autenticando seus usuários; o IAM é quem os building blocks consultam para saber a organização e as permissões. A integração natural é o SSO federar o Auth0 e o IAM emitir o token que o resto da plataforma entende.

Um usuário pode pertencer a mais de uma empresa?

Pode. Association é uma tabela com chave composta de usuário, organização e papel — a mesma pessoa pode ser ADMIN numa e VIEWER em outra. No login, informe organizationId para escolher o contexto; sem isso o serviço não tem como adivinhar qual você quer.

O que acontece se o token vazar?

Ele vale até expirar — uma hora por padrão. Não há revogação de access token individual, por design: revogar exigiria consultar o banco a cada requisição. Se o vazamento for confirmado, o caminho é revogar o refresh token e, no limite, rotacionar o JWT_SECRET, o que invalida todos os tokens de uma vez.

Dá para usar em produção com autenticação só por senha?

Depende do seu risco. Hoje não há MFA (§15). Para operações reguladas, a recomendação é federar por SSO em um provedor que já tenha MFA, em vez de esperar o segundo fator nativo.

Qual a diferença entre o IAM e o API Keys?

O IAM autentica identidades — pessoas e organizações — com token de vida curta. O API Keys emite credencial de longa duração para acesso programático, com rastreio de uso próprio. Integração que roda continuamente e precisa de escopo dinâmico usa M2M do IAM; integração simples com credencial estática usa API Keys.

Por que o organizationId do login aceita slug?

Conveniência de integração. Slug é legível e estável (financeira-exemplo), o que evita carregar UUID em script de setup e documentação. A resolução acontece no AuthService, e o token sempre carrega o UUID.

Como funciona o acesso do operador de plataforma?

Pelo claim isRoot. Contas root acessam entre organizações nas rotas que, para todos os outros, exigem que o recurso pertença à organização do token. É um privilégio elevado, deve ficar restrito a poucas contas, e todo uso deve ser acompanhado pelo Audit Trail.

Adicionar um building block novo exige mudar o IAM?

Não. O building block novo declara as permissões dele no vocabulário compartilhado e usa o requirePermission de sempre. O IAM não precisa saber o que o Commerce faz — só precisa carregar as strings no token.


Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md