Catalisa.
Building blocks/IdentidadeProdução

IAM

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

22
Endpoints
6
Entidades
0
Provedores
Global
Escopo
3001
Porta

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

01Resumo 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

02O problemanegó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.

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


03Proposta de valornegó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

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.


04Casos de uso reaisnegó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.

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.

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. O Audit Trail complementa registrando o que foi de fato acessado.

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

Como a Catalisa endereça. 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.

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


05Mercado e diferenciaisnegó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. Se o seu produto é B2C e precisa de login com Google e Apple, tela de login pronta, MFA por SMS e recuperação de senha com boa taxa de conversão, o Auth0 ou o Clerk entregam isso hoje e o IAM não entrega — nossa camada de identidade federada é um building block separado, o SSO, e o MFA ainda não está implementado. Se a exigência é SAML corporativo ou LDAP, o Keycloak é a escolha madura. E se você já vive dentro da AWS, tem volume alto de usuários e tolera a experiência de desenvolvimento, o Cognito será mais barato que qualquer alternativa. 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.


06Modelo de cobrança e ROInegó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 estimado2 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. 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.


07Arquitetura

                    HTTP
                      │
  ┌───────────────────┴────────────────────────────────────────────┐
  │ Hono app  basePath('/iam')                                      │
  │                                                                 │
  │  /api/v1/users          usersRouter · userAssociationsRouter    │
  │  /api/v1/organizations  organizationsRouter · membersRouter     │
  │  /api/v1/roles          rolesRouter                             │
  │  /api/v1/permissions    permissionsRouter                       │
  │  /api/v1/associations   associationsRouter                      │
  │  /oauth                 oauthRouter   (token · revoke)          │
  │  /health                sonda com banco, cripto e JWT           │
  └───────────────────┬────────────────────────────────────────────┘
                      │  Zod parse → ResultAsync<T, AppError>
  ┌───────────────────┴────────────────────────────────────────────┐
  │ services/                                                       │
  │   AuthService       login, emissão de par de tokens             │
  │   UserService       ciclo de vida do usuário                    │
  │   OrganizationService  organizações e client_secret             │
  │   AssociationService   usuário ↔ organização ↔ papel            │
  │   RoleService          catálogo de papéis e permissões          │
  │   OrganizationPermissionService  o que a org contratou          │
  │   RefreshTokenService  rotação e revogação                      │
  └───────────────────┬────────────────────────────────────────────┘
                      │
  ┌───────────────────┴────────────────────────────────────────────┐
  │ repositories/ (Prisma)  →  PostgreSQL, schema "iam"             │
  └─────────────────────────────────────────────────────────────────┘

  O token emitido aqui é consumido por TODOS os outros building blocks:

     ┌──────────┐   Bearer JWT    ┌──────────────────────────────┐
     │  Cliente │ ──────────────▶ │ authMiddleware               │
     └──────────┘                 │   verifica assinatura HS256  │
                                  │ requirePermission(P)         │
                                  │   P ∈ token.permissions?     │
                                  │ requireOrganization          │
                                  │   token.organizationId != ∅? │
                                  └──────────────────────────────┘
                                     ↑ mesmo código nos 32 BBs

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. A verificação de token não precisa de rede em nenhum dos dois modos: qualquer serviço com o JWT_SECRET valida a assinatura localmente.


08Conceitos 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

Ciclo de vida do usuário

   criação
      │
      ▼
  ┌─────────┐   ativação    ┌───────┐   suspensão   ┌───────────┐
  │ pendente│ ────────────▶ │ ativo │ ────────────▶ │ bloqueado │
  └─────────┘               └───┬───┘               └───────────┘
                                │  desativação
                                ▼
                            ┌─────────┐
                            │ inativo │
                            └─────────┘

  Usuário nasce `pendente`. Só `ativo` autentica.
  Exclusão é lógica: `deletedAt` preenchido, linha preservada para auditoria.

Como uma permissão chega ao token

  Permissões da organização        Permissões do papel
  (OrganizationPermission)         (Role → Permission)
            │                                │
            └───────────── ∩ ────────────────┘
                           │
                  interseção das duas
                           │
                           ▼
              token.permissions[]  ← o que o usuário
                                     de fato pode fazer

  Regra: 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.

09Referê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.

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

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

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

{
  "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 scopereduz: pedir uma permissão que a organização não contratou não a concede. O filtro está em filterScopedPermissions.

Resposta 200

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

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
401invalid_grantRefresh token inválido, expirado ou revogado
429Rate 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

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


10Início rápido

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

1. Autenticar

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}..."

2. Inspecionar o que o token carrega

echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq
{
  "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

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

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

4. Confirmar que o isolamento é real

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.


11Receitas

Criar uma organização e o primeiro administrador dela

Sequência completa de provisionamento de um cliente novo. Exige um token com permissões de IAM.

# 1. Criar a organização
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')

# 2. Criar o usuário
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')

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

# 4. Associar usuário + organização + papel
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}"

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

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

O parceiro usa assim:

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

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

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.

Descobrir por que uma requisição voltou 403

# O que o token realmente carrega
echo "$TOKEN" | cut -d. -f2 | base64 -d | jq '.permissions, .organizationId'

# O que a organização contratou
curl -s https://iam.stg.catalisa.app/iam/api/v1/permissions \
  -H "Authorization: Bearer $TOKEN" | jq

# O vocabulário completo, para conferir a grafia exata
curl -s https://iam.stg.catalisa.app/iam/api/v1/permissions/all \
  -H "Authorization: Bearer $TOKEN" | jq

Ordem de diagnóstico: a permissão existe no vocabulário? A organização contratou? O papel concede? O token foi emitido depois da mudança? Esta última é a causa mais comum — permissões são resolvidas na emissão, então mudança de papel só vale no próximo token.


12Integraçã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
  ┌─────────┐                    ┌──────────────────────────────────┐
  │ Usuário │──── login ────────▶│              IAM                 │
  └─────────┘                    │  emite access + refresh token    │
  ┌─────────┐  client_credentials│                                  │
  │ Parceiro│───────────────────▶│                                  │
  └─────────┘                    └────────────────┬─────────────────┘
                                                  │ Bearer JWT
       ┌──────────────┬──────────────┬────────────┴──┬──────────────┐
       ▼              ▼              ▼               ▼              ▼
  ┌─────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
  │Customers│   │ Payments │   │ Commerce │   │ Billing  │   │  ...27   │
  └─────────┘   └──────────┘   └──────────┘   └──────────┘   └──────────┘
       └──────────────┴──────────────┴───────────────┴──────────────┘
                 todos leem organizationId do token, nunca do corpo

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.


13Configuraçã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
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
403Building 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
429Rate 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.

14Seguranç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. 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. 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.

Senhas. Guardadas como hash pelo PasswordService compartilhado. Nunca retornadas, nunca logadas. O login limita a senha a 128 caracteres para evitar abuso de custo de hashing.

Segredos M2M. Guardados como hash em clientSecretHash, exibidos 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).


15Limitaçõ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

16Perguntas 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