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.
- 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
- 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
- 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.
| Atributo | Valor |
|---|---|
| Identificador | iam |
| Categoria | Identidade |
| Escopo | Global (não exige organizationId para operar) |
| Porta (standalone) | 3001 |
| Path alias | @iam |
| Prefixo HTTP | /iam |
| Status | Produção desde 2026-02 |
| Depende de | PostgreSQL, 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
| Antes | Depois |
|---|---|
| Cada serviço tem sua tabela de usuários e sua lógica de permissão | Um token, um vocabulário de permissões, 32 serviços obedecendo |
| "Lembrar de filtrar por organização" é disciplina de equipe | Building block tenant-scoped recusa requisição sem organização no token |
| Integração de parceiro usa usuário de serviço com senha fixa | client_credentials com escopo, credencial rotacionável, expiração curta |
| Adicionar um serviço novo custa duas semanas de autorização | O 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ério | Catalisa IAM | Auth0 | Amazon Cognito | Keycloak |
|---|---|---|---|---|
| Modelo de preço | Incluso na plataforma | Por usuário ativo mensal | Por usuário ativo mensal | Licença zero, você opera |
| Organização como cidadã de 1ª classe | Sim, no token | Só nos planos superiores | Não | Via realm, um por tenant |
| Autorização de domínio | Vocabulário único nos 32 BBs | Fora do escopo | Fora do escopo | Genérica, você mapeia |
| M2M com escopo | client_credentials nativo | Sim, cobrado à parte | Sim | Sim |
| Login social e SAML | Não (é o BB SSO) | Sim, extenso | Parcial | Sim, extenso |
| MFA | Não implementado (ver §15) | Sim | Sim | Sim |
| Tela de login hospedada | Não | Sim | Sim | Sim |
| Operação por sua conta | Já vem operado | Não | Não | Sim, integral |
Nossos diferenciais
- A permissão é do domínio, não genérica.
BILLING_INVOICES_VOIDsignifica 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. - 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. - 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. - 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 IAM | Auth0 (B2B Essentials) | Amazon Cognito | |
|---|---|---|---|
| Base de cálculo | Incluso na plataforma | Por usuário ativo mensal | Por usuário ativo mensal, após a faixa gratuita |
| Ordem de grandeza mensal | R$ 0 adicional | Alguns milhares de reais | Dezenas de reais |
| Autorização de domínio | Inclusa | Você implementa | Você implementa |
| Custo de engenharia estimado | — | 2 a 4 semanas por serviço novo | 2 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_SECRETcom 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.
isRootcomo 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/loginePOST /oauth/tokenpassam porauthRateLimitMiddleware, e falhas geram evento emlogSecurityEvent. 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
| Termo | Significa |
|---|---|
| Organization | Uma empresa dentro da plataforma. É a unidade de isolamento: todo dado tenant-scoped pertence a exatamente uma. |
| User | Uma pessoa. Existe globalmente, com e-mail único, e pode pertencer a várias organizações. |
| Association | O vínculo entre usuário, organização e papel. É o que responde "esta pessoa, nesta empresa, é o quê". |
| Role | Papel nomeado (ADMIN, VIEWER, ...). Agrupa permissões. |
| Permission | String do vocabulário compartilhado, no formato MODULO_RECURSO_ACAO — por exemplo IAM_USERS_CREATE. |
| OrganizationPermission | O que a organização contratou. Limite superior: um usuário nunca recebe permissão que a organização dele não tem. |
| Client secret | Segredo da organização para autenticação M2M. Guardado como hash, exibido só na criação. |
| isRoot | Marca de operador de plataforma. Libera acesso cross-organização. |
Modelo de dados — schema iam no PostgreSQL.
| Modelo Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
User | iam.users | Pessoa autenticável | email (único), passwordHash, status, isRoot, mfaEnabled, deletedAt |
Organization | iam.organizations | Empresa cliente, parceiro ou dono | slug (único), type, status, clientSecretHash, metadata |
Association | iam.associations | Vínculo usuário ↔ organização ↔ papel | Chave primária composta (userId, organizationId, roleId) |
Role | iam.roles | Catálogo de papéis | name (único) |
OrganizationPermission | iam.organization_permissions | Permissões contratadas pela organização | Único (organizationId, permission) |
RefreshToken | iam.refresh_tokens | Refresh tokens emitidos | token (único), expiresAt |
Enumerações
| Enum | Valores |
|---|---|
UserStatus | ativo · inativo · pendente · bloqueado |
OrganizationType | cliente · parceiro · dono |
OrganizationStatus | ativo · 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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /iam/api/v1/users | Cria usuário | IAM_USERS_CREATE |
POST | /iam/api/v1/users/login | Autentica e emite par de tokens | Pública (com rate limit) |
GET | /iam/api/v1/users | Lista usuários, paginado | IAM_USERS_READ |
GET | /iam/api/v1/users/:userId | Busca usuário | IAM_USERS_READ |
PATCH | /iam/api/v1/users/:userId | Atualiza usuário | IAM_USERS_UPDATE |
DELETE | /iam/api/v1/users/:userId | Exclusão lógica | IAM_USERS_DELETE |
GET | /iam/api/v1/users/:userId/associations | Organizações e papéis do usuário | IAM_ASSOCIATIONS_READ |
Organizações — /iam/api/v1/organizations
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /iam/api/v1/organizations | Cria organização | IAM_ORGANIZATIONS_CREATE |
GET | /iam/api/v1/organizations | Lista organizações | IAM_ORGANIZATIONS_READ |
GET | /iam/api/v1/organizations/:organizationId | Busca organização | IAM_ORGANIZATIONS_READ |
PATCH | /iam/api/v1/organizations/:organizationId | Atualiza organização | IAM_ORGANIZATIONS_UPDATE |
DELETE | /iam/api/v1/organizations/:organizationId | Exclusão lógica | IAM_ORGANIZATIONS_DELETE |
POST | /iam/api/v1/organizations/:organizationId/client-secret | Gera ou rotaciona o segredo M2M | IAM_ORGANIZATIONS_UPDATE |
GET | /iam/api/v1/organizations/:organizationId/members | Lista membros | IAM_ASSOCIATIONS_READ |
Papéis e permissões
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /iam/api/v1/roles | Lista papéis | IAM_ROLES_READ |
GET | /iam/api/v1/roles/:roleName/permissions | Permissões de um papel | IAM_ROLES_READ |
GET | /iam/api/v1/permissions | Permissões da organização atual | IAM_ROLES_READ |
GET | /iam/api/v1/permissions/all | Vocabulário completo da plataforma | IAM_ROLES_READ |
Associações — /iam/api/v1/associations
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /iam/api/v1/associations | Vincula usuário a organização com papel | IAM_ASSOCIATIONS_CREATE |
DELETE | /iam/api/v1/associations | Remove o vínculo | IAM_ASSOCIATIONS_DELETE |
OAuth 2.0 — /iam/oauth
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /iam/oauth/token | client_credentials ou refresh_token | Pública (com rate limit) |
POST | /iam/oauth/revoke | Revoga um refresh token | Pública |
Saúde
| Método | Rota | Descrição |
|---|---|---|
GET | /iam/health | Sonda 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"
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
email | string (e-mail) | Sim | E-mail do usuário |
password | string (máx. 128) | Sim | Senha em texto puro, sobre TLS |
organizationId | string | Não | Aceita 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. |
mfaCode | string | Não | Aceito 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
| Claim | Descrição |
|---|---|
sub | ID do usuário |
email | |
type | access ou refresh |
organizationId | Organização do contexto — o tenant efetivo da requisição |
permissions | Lista de permissões já resolvida |
isRoot | true só para operador de plataforma |
exp | Expiração — JWT_EXPIRES_IN, padrão 3600s |
Erros
| Status | Quando |
|---|---|
400 | Corpo não passa no schema Zod |
401 | Credenciais inválidas, usuário não ativo, ou sem acesso à organização informada |
429 | Rate 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"
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
grant_type | client_credentials | refresh_token | Sim | Fluxo |
client_id | string | Em client_credentials | É o organizationId |
client_secret | string | Em client_credentials | Segredo gerado na rota de client-secret |
scope | string | Não | Permissões separadas por espaço. Omitido, o token recebe todas as permissões da organização. |
refresh_token | string | Em refresh_token | O 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
{
"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.
| Status | error | Quando |
|---|---|---|
400 | unsupported_grant_type | grant_type fora dos suportados |
401 | invalid_client | client_id/client_secret ausente ou inválido |
401 | invalid_grant | Refresh token inválido, expirado ou revogado |
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
{
"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
pendentee não autentica até virarativo— faça oPATCHde status ou o login falha com401e ninguém entende por quê. - Sem a associação do passo 4, o login retorna
401mesmo com senha correta: usuário sem organização não tem contexto. - A organização precisa ter as permissões contratadas em
OrganizationPermission. PapelADMINnuma 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 retorna409.
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
.envversionado — 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 block | Como se relaciona | Obrigatório |
|---|---|---|
| Todos os 31 demais | Verificam o token emitido aqui via authMiddleware | Sim |
| API Keys | Alternativa ao JWT para acesso programático de longa duração | Não |
| SSO | Identidade federada (login social, provedores externos) apoiada no IAM | Não |
| Audit Trail | Registra o que cada identidade fez; o IAM define quem ela é | Não |
| Webhooks Engine | Escopa entrega de eventos por organização | Nã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ável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
JWT_SECRET | Segredo HS256. Mínimo 44 caracteres — o serviço não sobe abaixo disso. Gere com openssl rand -base64 32. | Sim | — |
JWT_EXPIRES_IN | Vida do access token, em segundos | Não | 3600 |
JWT_REFRESH_EXPIRES_IN | Vida do refresh token, em segundos | Não | 604800 |
DATABASE_URL | PostgreSQL | Sim | — |
REDIS_URL | Redis, usado no rate limit | Sim | — |
DEPLOYMENT_MODE | monolith ou standalone | Não | monolith |
Dependências de infraestrutura
| Dependência | Para quê |
|---|---|
| PostgreSQL | Schema iam — usuários, organizações, associações, refresh tokens |
| Redis | Contadores de rate limit das rotas de autenticação |
Limites
| Limite | Valor |
|---|---|
| Comprimento da senha no login | 128 caracteres |
Comprimento mínimo do JWT_SECRET | 44 caracteres |
| Rate limit de autenticação | Definido em authRateLimitMiddleware |
| Vida do access token | 1 hora (padrão) |
| Vida do refresh token | 7 dias (padrão) |
Catálogo de erros
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | VALIDATION | Corpo reprovado no Zod | Confira campos e tipos contra §9 |
401 | UNAUTHORIZED | Token ausente, inválido ou expirado | Renove pelo refresh token |
401 | invalid_client | Credencial M2M inválida (ou organização inexistente) | Confira client_id e rotacione o segredo |
403 | FORBIDDEN | Token válido, permissão faltando | Siga a receita "por que voltou 403" em §11 |
403 | — | Building block tenant-scoped sem organizationId no token | Autentique informando a organização |
404 | NOT_FOUND | Recurso inexistente ou excluído logicamente | Confira o ID |
409 | CONFLICT | E-mail ou slug já em uso | Escolha outro |
429 | — | Rate limit de autenticação | Aplique recuo exponencial |
503 | — | /health reprovou banco, cripto ou JWT | Verifique Postgres e JWT_SECRET |
Observabilidade.
GET /iam/healthvalida conectividade com o banco, o subsistema de senha e um round-trip completo de assinar/verificar JWT. Responde503se qualquer um falhar — é a sonda certa para orquestrador.logSecurityEventregistraLOGIN_SUCCESS,LOGIN_FAILUREeOAUTH_TOKEN_FAILUREcom 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ção | Impacto | Situação |
|---|---|---|
| MFA não implementado | User.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 SAML | Autenticação é e-mail e senha ou M2M | Por design — é o building block SSO |
| Sem tela de login hospedada | O cliente constrói a própria interface | Por design |
| Permissões resolvidas na emissão | Mudanç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-mail | Reset é PATCH administrativo | Roadmap |
| Sem expurgo automatizado para LGPD | Exclusão é lógica; eliminação definitiva é manual | Roadmap |
| HS256 com segredo compartilhado | Qualquer serviço com o segredo pode emitir token, não só verificar | Por design enquanto todos os consumidores forem internos |
| Sem cadastro self-service | Organização e usuário são criados por quem já tem permissão de IAM | Especificado, 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