Catalisa.Building Blocks
Catálogo/Identidade/SSO

SSO

Beta

Login com Google, Microsoft, GitHub e Meta para as empresas que você atende

14
Endpoints
2
Entidades
4
Provedores
Tenant
Escopo
3022
Porta
2026-02
Desde

Cada empresa cliente entra na sua plataforma com a conta que ela já usa — Google Workspace, Microsoft Entra, GitHub ou Meta — e você configura isso por organização, sem escrever integração OAuth nenhuma.

Para quem é
  • Plataformas B2B cujas empresas clientes já vivem dentro do Google Workspace ou do Microsoft 365
  • Fintechs e financeiras que precisam restringir acesso ao domínio de e-mail corporativo do parceiro
  • Times de produto que hoje mantêm integração OAuth escrita à mão para cada provedor
Substitui
  • Assinatura de identidade federada por conexão (WorkOS, add-on de Enterprise Connection do Auth0 ou do Clerk)
  • Código de integração OAuth 2.0 escrito e mantido dentro da aplicação, um por provedor
  • Tabela caseira que amarra o usuário externo ao usuário interno
O que não é
  • Um provedor de identidade — o SSO consome provedores externos, não hospeda contas
  • Suporte a SAML ou SCIM: hoje só há OAuth 2.0 e OpenID Connect (ver §15)
  • Substituto do IAM — quem emite o token que os building blocks verificam continua sendo o IAM
O que dá para fazer

15 endpoints em 4 recursos.

Explorar a API →
01

Resumo executivo

O SSO deixa o usuário entrar na sua plataforma com a conta que ele já tem. Em vez de criar mais uma senha, ele clica em "Entrar com Microsoft", autentica no provedor da própria empresa e volta com uma identidade que a plataforma reconhece.

O que muda na prática: uma financeira que contrata a sua plataforma e usa Google Workspace pede que os operadores dela entrem com o e-mail corporativo. Você habilita o Google para aquela organização, restringe ao domínio @financeira.com.br, e ninguém de fora daquele domínio consegue entrar por esse caminho — sem uma linha de código nova.

Está em beta. O fluxo de autenticação federada funciona ponta a ponta e devolve uma asserção assinada, mas duas peças ainda faltam para o ciclo fechar sozinho: o endpoint do IAM que troca a asserção por um access token e o vínculo automático da identidade externa com o usuário interno. Ambas estão detalhadas na §15, e enquanto não existirem a aplicação precisa fazer esse último passo. Leia essa seção antes de planejar a integração.

AtributoValor
Identificadorsso
CategoriaIdentidade
EscopoTenant (exige organizationId no token nas rotas administrativas)
Porta (standalone)3022
Path alias@sso
Prefixo HTTP/sso
StatusBeta desde 2026-02
Depende dePostgreSQL, Redis, IAM

02

O problema

negócio

O cenário. Você vende uma plataforma para empresas. Cada empresa cliente tem um time de segurança, e esse time tem uma regra: ninguém cria senha nova em ferramenta de terceiro. Acesso corporativo passa pelo provedor de identidade da casa — Google Workspace, Microsoft Entra ID — porque é lá que o desligamento de um funcionário corta tudo de uma vez.

Sem SSO, a conversa comercial trava nesse ponto. E a alternativa que a maioria dos times escolhe — escrever a integração OAuth à mão — parece pequena até você precisar da segunda.

flowchart TD
  P["Plataforma B2B"] --> G["Integração OAuth Google<br/>escrita à mão"]
  P --> M["Integração OAuth Microsoft<br/>escrita à mão"]
  P --> GH["Integração OAuth GitHub<br/>escrita à mão"]
  G --> S1["Guarda client_secret onde?"]
  M --> S2["PKCE implementado certo?"]
  GH --> S3["State validado contra CSRF?"]
  S1 --> R["Três implementações,<br/>três superfícies de erro"]
  S2 --> R
  S3 --> R

O que trava hoje.

  • Cada provedor é uma integração diferente. O Google devolve email_verified no perfil; o GitHub exige uma segunda chamada só para descobrir o e-mail; o Microsoft muda a URL conforme o tenant. Três provedores são três códigos, cada um com o próprio jeito de errar.
  • O client_secret da empresa cliente precisa morar em algum lugar. Se cada cliente traz o próprio par de credenciais, guardar isso em variável de ambiente para de escalar no segundo cliente, e guardar em texto puro no banco é achado de auditoria garantido.
  • Os detalhes de segurança do OAuth são fáceis de errar em silêncio. state sem validação é CSRF de login. PKCE ausente é código de autorização interceptável. Nenhum dos dois quebra nenhum teste.
  • Restringir por domínio de e-mail é requisito comum e some do escopo. "Só quem tem @cliente.com.br entra" parece detalhe até o dia em que alguém entra com Gmail pessoal e vê dado de crédito.
  • Habilitar um provedor para um cliente vira deploy. Se a configuração está no código, ligar o Microsoft para a empresa 47 exige alguém empacotar, revisar e subir versão.

O custo de não resolver. O mercado precifica exatamente essa dor, e o preço é público. A WorkOS cobra US$ 125 por conexão de SSO por mês na primeira faixa (WorkOS Pricing, consultado em 2026-08-17). O Auth0 cobra US$ 100 por mês por conexão corporativa adicional, acima das 3 inclusas no B2B Essentials (Auth0 Pricing, consultado em 2026-08-17). Numa plataforma com 40 empresas clientes que pedem login corporativo, isso é linha de custo recorrente que cresce com a sua base — e o número é o mesmo se a conexão for usada por 3 pessoas ou por 300.


03

Proposta de valor

negócio
AntesDepois
Uma integração OAuth escrita à mão por provedorQuatro provedores atrás da mesma API, com PKCE e validação de state embutidos
client_secret do cliente em variável de ambiente ou texto puroCifrado com AES-256-GCM no banco, nunca devolvido em resposta
Habilitar provedor para um cliente exige deployPOST /sso/api/v1/sso/providers e o provedor aparece na tela de login daquela organização
Restrição por domínio de e-mail é código ad hocCampo allowedDomains na configuração, verificado no callback
Custo por conexão que cresce com a base de clientesCusto desacoplado do número de organizações

O desenho por trás dessa tabela é uma inversão simples: a configuração sai do código e vai para o banco, com uma linha por organização.

flowchart LR
  subgraph antes["Antes — configuração no código"]
    C1["Cliente A"] --> COD["if (cliente === 'A') ..."]
    C2["Cliente B"] --> COD
    COD --> DEP["Deploy para cada mudança"]
  end
  subgraph depois["Depois — configuração no banco"]
    O1["Organization A"] --> T["SsoProviderConfig<br/>1 linha por org e provedor"]
    O2["Organization B"] --> T
    T --> API["Mudança é chamada de API"]
  end

Quatro provedores, uma API

GOOGLE, MICROSOFT, GITHUB e META implementam a mesma interface interna. Quem integra chama GET /sso/api/v1/sso/auth/:provider/authorize e recebe a URL de autorização pronta, seja qual for o provedor.

PKCE onde o provedor suporta

Google, Microsoft e Meta recebem code_challenge com método S256, derivado de um verificador de 32 bytes aleatórios. O GitHub, que não suporta PKCE no fluxo web, é tratado como exceção declarada no código, não como esquecimento.

O segredo do cliente nunca sai do banco em claro

credentials é um JSON cifrado com AES-256-GCM, no formato iv:authTag:ciphertext. Nenhuma rota devolve esse campo: o serializador da API expõe name, providerType, scopes, redirectUri, allowedDomains, autoCreateUser, status e settings — e nada mais.

Restrição por domínio antes de a identidade existir

Se allowedDomains está preenchido, o callback compara o domínio do e-mail devolvido pelo provedor e responde 403 antes de gravar qualquer coisa. Quem não é do domínio não vira registro.


04

Casos de uso reais

negócio

Caso 1 — Uma financeira exige que o correspondente entre pelo Google Workspace da empresa dele Cenário ilustrativo

Contexto

Financeira de crédito consignado que opera com correspondentes bancários. O maior deles tem 60 operadores e um time de TI que administra Google Workspace.

A dor

O correspondente recusou o modelo de senha própria. O argumento foi específico e correto: quando um operador é desligado, a TI dele revoga a conta Google e precisa que isso corte o acesso a todos os sistemas no mesmo instante. Com senha separada na plataforma, existia uma janela entre o desligamento e alguém lembrar de desativar o usuário lá.

A solução com o BB

A financeira cria uma SsoProviderConfig do tipo GOOGLE para a organização daquele correspondente, com allowedDomains: ["correspondente.com.br"]. A tela de login consulta GET /sso/api/v1/sso/providers/available?organization_id=... e mostra o botão do Google só para essa organização.

sequenceDiagram
  participant Adm as Admin da financeira
  participant SSO
  participant Login as Tela de login
  Adm->>SSO: POST /providers (GOOGLE, allowedDomains, credentials)
  SSO-->>Adm: 201 — credentials cifrado, nunca devolvido
  Login->>SSO: GET /providers/available?organization_id=...
  SSO-->>Login: [{ providerType GOOGLE, name }]
  Note over Login: botão "Entrar com Google" aparece<br/>só para esta organização
O resultado

O desligamento no Google Workspace passa a ser suficiente. E se alguém tentar entrar com Gmail pessoal, o callback responde 403 porque o domínio não está na lista.

Caso 2 — Um administrador conecta a identidade externa ao usuário interno Cenário ilustrativo

Contexto

Empresa cliente com 200 usuários já cadastrados na plataforma que decide ligar o Microsoft Entra ID.

A dor

Ligar o SSO num sistema com base existente cria um problema de correspondência: a pessoa que entra como maria@empresa.com.br pelo Entra é a mesma maria@empresa.com.br que já existia como usuário. Sem tratar isso, o resultado são dois cadastros e um histórico partido ao meio.

A solução com o BB

Toda autenticação federada cria ou atualiza uma SsoIdentity, com subjectId nulo. O administrador lista as identidades sem vínculo e amarra cada uma ao usuário interno correspondente com PUT /sso/api/v1/sso/identities/:id/link.

stateDiagram-v2
  [*] --> NaoVinculada: primeiro login federado<br/>subjectId = null
  NaoVinculada --> Vinculada: PUT /identities/:id/link
  Vinculada --> NaoVinculada: DELETE /identities/:id/link
  NaoVinculada --> [*]: DELETE /identities/:id
  Vinculada --> [*]: DELETE /identities/:id
O resultado

Uma pessoa, um usuário interno, e o histórico preservado. A partir do vínculo, a asserção emitida no login já carrega o subjectId, e a aplicação sabe imediatamente para qual usuário interno apontar.

Caso 3 — Uma plataforma para de pagar por conexão de SSO Cenário ilustrativo

Contexto

Plataforma B2B com 40 empresas clientes, das quais 18 pedem login corporativo.

A dor

Com preço por conexão, o custo do login corporativo cresce junto com a base — que é exatamente o eixo em que a plataforma quer crescer. Pior: o custo é o mesmo para o cliente de 3 usuários e para o de 300, então os clientes pequenos ficam com margem negativa só na linha de identidade.

A solução com o BB

Cada empresa vira uma linha em SsoProviderConfig. Habilitar uma organização nova é um INSERT, não um contrato novo.

Por conexão (referência de mercado)Catalisa SSO
Base de cálculoConexão ativa por mêsConfiguração no banco
18 conexões18 × preço de tabela do fornecedorMesmo custo de infraestrutura
Custo marginal da 19ªMais uma conexão cobradaUma linha no banco
O resultado

O custo de identidade federada deixa de ser função do número de clientes. A contrapartida honesta: o SSO da Catalisa cobre quatro provedores OAuth, e não o catálogo completo de SAML e SCIM que justifica aquele preço — ver §5 e §15.

Caso 4 — O sobrepreço de SSO é um problema documentado publicamente Referência de mercado

Contexto

Existe um catálogo público de fornecedores que tratam SSO como item de luxo: o SSO Wall of Shame, mantido por Rob Chahin (consultado em 2026-08-17). Ele lista o preço do plano base contra o preço do primeiro plano que libera SAML.

A dor do mercado

A metodologia declarada no site aceita cerca de 10% de acréscimo como custo legítimo de manutenção e sinaliza o que passa muito disso. Os casos listados são de outra ordem de grandeza — por exemplo Appsmith (US$ 15 → US$ 2.500 por usuário/mês), Railway (US$ 20 → US$ 2.000) e Mixpanel (US$ 20 → US$ 833/mês).

Atenção. Esses números são do site citado, referem-se a outros fornecedores e não à Catalisa. Servem para mostrar que a prática é conhecida e catalogada, não como comparação de preço com nenhum produto nosso.

Como a Catalisa endereça

O SSO é um building block do catálogo, não um degrau de plano. Não existe uma versão da plataforma em que ele esteja desligado por razão comercial — a limitação real é de cobertura de protocolo, e está escrita na §15.

O resultado

O comprador consegue auditar a afirmação: os quatro provedores implementados estão nomeados, o que falta está nomeado, e nada disso depende de faixa de contrato.


05

Mercado e diferenciais

negócio

Panorama. O mercado de identidade federada se organizou em três respostas. A primeira é o provedor especializado em B2B — WorkOS é o caso mais direto — que vende conexão como unidade e entrega SAML, OIDC e sincronização de diretório com um portal para o cliente final se configurar. A segunda é o provedor de identidade completo — Auth0, Clerk — em que a federação é um recurso dentro de um produto maior, cobrada por usuário ativo mais um adicional por conexão corporativa. A terceira é o auto-hospedado, com o Keycloak à frente, que entrega cobertura de protocolo completa e transfere a operação inteira para você.

O SSO da Catalisa é deliberadamente mais estreito que os três. Ele resolve OAuth 2.0 e OpenID Connect com quatro provedores, configurados por organização, e entrega isso já acoplado ao IAM que o resto do catálogo usa.

CritérioCatalisa SSOWorkOSAuth0Keycloak
Modelo de preçoIncluso na plataformaUS$ 125/conexão/mês na 1ª faixaPor usuário ativo + US$ 100/conexão adicionalLicença zero, você opera
OAuth 2.0 / OIDCSim, 4 provedoresSimSim, catálogo amploSim
SAML 2.0Não (ver §15)SimSimSim
SCIM / Directory SyncNão (ver §15)SimSimParcial
Configuração por organizaçãoSim, no bancoSim, via admin portalSó nos planos B2BVia realm, um por cliente
Restrição por domínio de e-mailSim, allowedDomainsSimSimSim
Credenciais do cliente cifradas em repousoSim, AES-256-GCMGerenciado pelo fornecedorGerenciado pelo fornecedorDepende da sua configuração
Portal para o cliente final se configurarNão (ver §15)SimParcialSim, console admin
Operação por sua contaJá vem operadaNãoNãoSim, integral

Preços de WorkOS e Auth0 conforme as páginas públicas (WorkOS, Auth0), consultadas em 2026-08-17. Variam por volume, região e negociação.

Nossos diferenciais

  1. A configuração é por organização e vive no banco. Ligar o Microsoft para a empresa 47 é uma chamada de API. Isso é difícil de copiar não pelo código, mas porque exige que o conceito de organização já seja cidadão de primeira classe em toda a plataforma — o que vem do IAM, não do SSO.
  2. O custo não escala com o número de empresas clientes. É a inversão exata do modelo por conexão. Um fornecedor cujo faturamento é conexão × preço não tem como oferecer isso sem competir com a própria receita.
  3. A credencial do cliente é cifrada com chave que não está no banco. SSO_CREDENTIAL_MASTER_KEY é variável de ambiente de 32 bytes. Dump do banco sem a chave não devolve nenhum client_secret.

Quando escolher o concorrente. Na maioria das negociações corporativas grandes, a resposta honesta é WorkOS ou Keycloak — e não SSO da Catalisa. A régua é o protocolo que o cliente exige.

flowchart TD
  Q1{"O cliente exige<br/>SAML 2.0?"}
  Q1 -->|sim| W["WorkOS ou Keycloak"]
  Q1 -->|não| Q2{"Precisa de SCIM para<br/>provisionar e desprovisionar<br/>usuários automaticamente?"}
  Q2 -->|sim| W2["WorkOS"]
  Q2 -->|não| Q3{"O cliente final precisa<br/>configurar sozinho,<br/>sem passar por você?"}
  Q3 -->|sim| W3["WorkOS (admin portal)"]
  Q3 -->|não| S["Catalisa SSO"]
Se o seu caso éEscolhaPor quê
Cliente corporativo que exige SAML 2.0, ADFS ou Okta como IdPWorkOS ou KeycloakO SSO da Catalisa não implementa SAML, e isso não tem contorno
Provisionamento automático de usuários por SCIMWorkOSNão implementamos Directory Sync
O time de TI do cliente quer configurar a conexão sozinho, num portalWorkOSNão temos portal self-service; a configuração passa por quem tem SSO_ADMIN
Cobertura máxima de protocolo, com equipe para operarKeycloakOIDC, OAuth 2.0, SAML 2.0 e federação LDAP, sob licença aberta (keycloak.org, consultado em 2026-08-17)
Google, Microsoft, GitHub ou Meta, configurado por organização, dentro do catálogo CatalisaCatalisa SSOÉ exatamente o recorte implementado

06

Modelo de cobrança e ROI

negócio

Unidade de cobrança. Precificação em definição. O SSO ainda está em beta e não tem preço fechado. O que já está definido é o que não será cobrado: conexão ativa. Cobrar por conexão é o modelo que cria o incentivo errado — o cliente adia habilitar login corporativo para empresas pequenas, que são justamente as que mais sofrem com senha compartilhada.

O que dispara custo. Dois drivers, ambos de impacto marginal na infraestrutura:

DriverPor que importaOrdem de grandeza
Organizações com provedor ativoUma linha em SsoProviderConfig por organização e provedorCusto de armazenamento desprezível
Autenticações federadas por mêsCada login gasta uma chave no Redis por até SSO_STATE_TTL e faz 2 chamadas HTTP ao provedorLatência dominada pelo provedor externo

Comparação de custo — cenário: plataforma B2B com 40 empresas clientes, das quais 18 pedem login corporativo.

Catalisa SSOWorkOSAuth0 (B2B Essentials)
Base de cálculoIncluso na plataformaPor conexão de SSO por mêsPor usuário ativo + conexão corporativa adicional
Preço público de referência—US$ 125/conexão na faixa de 1 a 15; US$ 100 na faixa de 16 a 303 conexões inclusas; US$ 100/mês por conexão adicional
18 conexõesSem custo adicional por conexão18 conexões na faixa de US$ 100 cada3 inclusas + 15 adicionais
Custo marginal da 19ª empresaUma linha no bancoMais uma conexão cobradaMais uma conexão cobrada

Estimativa para orientar conversa, não proposta comercial. Preços públicos de WorkOS e Auth0 consultados em 2026-08-17; ambos mudam por volume e negociação. O cálculo acima ignora o custo por usuário ativo do Auth0, que existe e é separado.

ROI. O retorno tem duas parcelas e é honesto separá-las. A primeira é a linha de licença que não aparece — real, mas só se materializa para quem tem muitas empresas clientes pedindo federação. A segunda, e a maior, é a engenharia que não é escrita: quatro integrações OAuth com PKCE, validação de state, cifra de credencial e restrição por domínio, mais a manutenção delas conforme cada provedor muda a própria API. Contra isso, o custo de adoção do SSO é preencher uma configuração por organização.


07

Arquitetura

As camadas e o caminho da requisição

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

  subgraph HONO["Hono app — basePath('/sso')"]
    R1["/api/v1/sso/providers<br/>providerConfigRouter — 7 rotas"]
    R2["/api/v1/sso/identities<br/>identityRouter — 5 rotas"]
    R3["/api/v1/sso/auth<br/>authFlowRouter — 2 rotas públicas"]
    R4["/health"]
  end

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

  subgraph SVC["services/"]
    S1["SsoProviderConfigService<br/>CRUD e teste de credencial"]
    S2["SsoIdentityService<br/>identidades e vínculo com subject"]
    S3["SsoAuthService<br/>initiateAuth e handleCallback"]
  end

  SVC --> PROV

  subgraph PROV["providers/ — implementam IOAuthProvider"]
    P1["GoogleProvider"]
    P2["MicrosoftProvider"]
    P3["GitHubProvider"]
    P4["MetaProvider"]
  end

  SVC --> REPO["repositories/ (Prisma)"]
  REPO --> PG[("PostgreSQL — schema sso")]
  S3 --> RD[("Redis — sso:state:*")]
  PROV --> EXT["Provedor externo<br/>accounts.google.com, login.microsoftonline.com, ..."]

O fluxo de autenticação federada

Este é o coração do building block. Duas rotas públicas, uma ida ao provedor e uma asserção assinada de volta.

sequenceDiagram
  autonumber
  participant U as Usuário
  participant App as Sua aplicação
  participant SSO as SSO (BB)
  participant RD as Redis
  participant IdP as Provedor (Google, Microsoft, ...)

  U->>App: clica em "Entrar com Google"
  App->>SSO: GET /auth/GOOGLE/authorize?organization_id&redirect_uri
  SSO->>SSO: carrega SsoProviderConfig e decifra credentials
  SSO->>SSO: gera state, nonce e code_verifier (PKCE)
  SSO->>RD: SET sso:state:{state} (TTL SSO_STATE_TTL)
  SSO-->>App: { authorizationUrl, state }
  App-->>U: redireciona para authorizationUrl
  U->>IdP: autentica no provedor
  IdP-->>SSO: GET /auth/callback?code&state
  SSO->>RD: GET e DEL sso:state:{state} (uso único)
  SSO->>IdP: troca code por tokens (com code_verifier)
  IdP-->>SSO: access_token
  SSO->>IdP: busca perfil do usuário
  IdP-->>SSO: externalId, email, emailVerified, nome, avatar
  SSO->>SSO: valida allowedDomains (403 se fora)
  SSO->>SSO: upsert SsoIdentity e assina a asserção (HS256)
  SSO-->>U: 302 para redirect_uri?sso_assertion=...
  U->>App: chega com a asserção
  Note over App: a aplicação valida a asserção e<br/>decide qual usuário interno ela representa<br/>(ver §15 — a troca por token do IAM<br/>ainda não é um endpoint do IAM)

Decisões não óbvias.

  • O state vive no Redis, não em cookie. Um cookie exigiria que o /authorize e o /callback compartilhassem domínio e sessão, o que não vale quando o /authorize é chamado por uma SPA e o /callback é chamado pelo provedor. O trade-off é a dependência do Redis no caminho do login — sem Redis, ninguém autentica.
  • O state é consumido uma única vez. O handleCallback lê a chave e apaga em seguida. Isso limita a janela de replay do código de autorização à duração de uma requisição.
  • PKCE é por provedor, declarado em tabela. GOOGLE, MICROSOFT e META recebem code_challenge; o GITHUB não, porque não suporta. Deixar isso como mapa explícito no código evita que a ausência de PKCE no GitHub pareça esquecimento numa revisão futura.
  • A asserção é um JWT de tipo próprio, e não um access token. O payload carrega type: 'sso_assertion'. Isso importa: o authMiddleware da plataforma recusa qualquer token cujo type não seja access, então a asserção não abre porta em nenhum building block. Ela serve só para provar quem autenticou.
  • A asserção vive SSO_ASSERTION_TTL segundos — 300 por padrão. Ela viaja na barra de endereços do navegador, e query string entra em log de proxy e em histórico. Vida curta é o que limita o estrago disso.
  • O provedor é resolvido por fábrica, não por herança. createOAuthProvider devolve Result<IOAuthProvider, AppError>: um providerType desconhecido vira erro tratado, não exceção.

Monolito vs. standalone. Nos dois modos o prefixo é o mesmo, porque o basePath('/sso') está no app.ts do módulo e o monolito monta esse app inteiro. Em standalone o serviço sobe na porta 3022 conforme DEFAULT_MODULE_PORTS. O applyCommonMiddleware — limite de 1 MB de corpo, CORS, cabeçalhos de segurança e rate limit global — é aplicado no próprio app.ts, então vale nos dois modos.


08

Conceitos e modelo de dados

Glossário

TermoSignifica
Provider configA configuração de um provedor para uma organização. Guarda credenciais cifradas, escopos, URI de retorno e domínios permitidos.
Provider typeQual provedor externo: GOOGLE, MICROSOFT, GITHUB ou META. Não há outros implementados.
IdentityA identidade de uma pessoa naquele provedor, naquela configuração. Chave natural: providerConfigId + externalId.
External IDO identificador da pessoa dentro do provedor. É o sub do Google, o id do GitHub — nunca o e-mail.
SubjectO usuário interno da plataforma ao qual a identidade externa foi vinculada. Guardado em subjectId, nulo até alguém vincular.
StateValor aleatório de 32 bytes que amarra o /authorize ao /callback e protege contra CSRF de login. Vive no Redis, uso único.
PKCEProof Key for Code Exchange (RFC 7636). O code_verifier fica no state; o code_challenge vai ao provedor.
Asserção SSOJWT de curta duração, assinado em HS256, que o callback devolve provando quem autenticou. Não é access token.
Allowed domainsLista de domínios de e-mail aceitos. Vazia significa qualquer domínio.
Auto create userSinalizador que viaja na asserção indicando a intenção de criar o usuário interno automaticamente. Hoje é apenas transportado — ver §15.

Modelo de dados — schema sso no PostgreSQL.

Modelo PrismaTabelaPropósitoCampos-chave
SsoProviderConfigsso.sso_provider_configsConfiguração de um provedor para uma organizaçãoorganizationId, providerType, credentials (cifrado), redirectUri, allowedDomains, autoCreateUser, status, settings, deletedAt. Único (organizationId, providerType)
SsoIdentitysso.sso_identitiesIdentidade de uma pessoa em um provedorproviderConfigId, externalId, email, emailVerified, subjectId, rawProfile, lastLoginAt. Único (providerConfigId, externalId)

Enumerações

EnumValoresObservação
SsoProviderTypeGOOGLE · MICROSOFT · GITHUB · METAO enum do Prisma e a fábrica de provedores têm exatamente esses quatro
SsoProviderConfigStatusACTIVE · INACTIVESó ACTIVE inicia fluxo de autenticação

Ciclo de vida da configuração de provedor

stateDiagram-v2
  [*] --> ACTIVE: POST /providers<br/>(status ACTIVE por padrão)
  ACTIVE --> INACTIVE: PATCH /providers/:id (status INACTIVE)
  INACTIVE --> ACTIVE: PATCH /providers/:id (status ACTIVE)
  ACTIVE --> Excluida: DELETE /providers/:id
  INACTIVE --> Excluida: DELETE /providers/:id
  Excluida --> [*]
  note right of ACTIVE
    Só ACTIVE aparece em
    /providers/available e
    permite /auth/:provider/authorize
  end note
  note right of Excluida
    Exclusão lógica: deletedAt
    preenchido, linha preservada
  end note

O que cada provedor entrega no perfil

Os quatro provedores devolvem a mesma estrutura interna, mas a origem de cada campo muda — e emailVerified é onde a diferença importa.

ProvedorEscopos padrãoPKCEOrigem do emailVerified
GOOGLEopenid, email, profileSim (S256)Campo email_verified do userinfo
MICROSOFTopenid, email, profile, User.ReadSim (S256)Fixo em true — o Microsoft Graph só devolve e-mail verificado
GITHUBread:user, user:emailNãoCampo verified do e-mail primário, via /user/emails
METAemail, public_profileSim (S256)Verdadeiro se o e-mail veio preenchido

Atenção. O MICROSOFT aceita settings.tenant para apontar a um tenant específico do Entra ID. Sem esse ajuste, o valor é common, que aceita conta de qualquer tenant e conta pessoal Microsoft. Se a intenção é restringir a uma empresa, defina settings.tenant e allowedDomains.


09

Referência da API

Prefixo: /sso. Repare que o segmento sso aparece duas vezes na rota — uma vez do basePath('/sso') do módulo e outra do caminho em que os routers são montados. As rotas abaixo estão completas e literais; é assim que elas respondem.

Configurações de provedor — /sso/api/v1/sso/providers

MétodoRotaDescriçãoPermissão
POST/sso/api/v1/sso/providersCria a configuração de um provedor para a organizaçãoSSO_ADMIN
GET/sso/api/v1/sso/providersLista as configurações da organizaçãoSSO_READ
GET/sso/api/v1/sso/providers/availableProvedores ativos, para montar a tela de loginPública
GET/sso/api/v1/sso/providers/:idBusca uma configuraçãoSSO_READ
PATCH/sso/api/v1/sso/providers/:idAtualiza a configuraçãoSSO_ADMIN
DELETE/sso/api/v1/sso/providers/:idExclusão lógicaSSO_ADMIN
POST/sso/api/v1/sso/providers/:id/testValida as credenciais gravadasSSO_ADMIN

Todas as rotas acima, exceto /available, exigem authMiddleware e requireOrganization — token sem organizationId recebe 403.

Identidades — /sso/api/v1/sso/identities

MétodoRotaDescriçãoPermissão
GET/sso/api/v1/sso/identitiesLista identidades da organização, paginadoSSO_IDENTITIES_READ
GET/sso/api/v1/sso/identities/:idBusca uma identidadeSSO_IDENTITIES_READ
PUT/sso/api/v1/sso/identities/:id/linkVincula a identidade a um usuário internoSSO_IDENTITIES_MANAGE
DELETE/sso/api/v1/sso/identities/:id/linkDesfaz o vínculoSSO_IDENTITIES_MANAGE
DELETE/sso/api/v1/sso/identities/:idRemove a identidadeSSO_IDENTITIES_MANAGE

Todas exigem authMiddleware e requireOrganization.

Fluxo de autenticação — /sso/api/v1/sso/auth

MétodoRotaDescriçãoPermissão
GET/sso/api/v1/sso/auth/:provider/authorizeInicia o fluxo e devolve a URL de autorizaçãoPública
GET/sso/api/v1/sso/auth/callbackRecebe o retorno do provedor e redireciona com a asserçãoPública

Saúde

MétodoRotaDescrição
GET/sso/healthSonda de disponibilidade do serviço

POST /sso/api/v1/sso/providers

Cria a configuração de um provedor. Aceita o corpo direto ou embrulhado em data.attributes (JSON:API).

Request

json
{
  "name": "Google Workspace da Financeira Exemplo",
  "providerType": "GOOGLE",
  "credentials": {
    "client_id": "<client_id do provedor>",
    "client_secret": "<client_secret do provedor>"
  },
  "redirectUri": "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/auth/callback",
  "scopes": ["openid", "email", "profile"],
  "allowedDomains": ["financeira-exemplo.com.br"],
  "autoCreateUser": false,
  "status": "ACTIVE"
}
{
  "name": "Google Workspace da Financeira Exemplo",
  "providerType": "GOOGLE",
  "credentials": {
    "client_id": "<client_id do provedor>",
    "client_secret": "<client_secret do provedor>"
  },
  "redirectUri": "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/auth/callback",
  "scopes": ["openid", "email", "profile"],
  "allowedDomains": ["financeira-exemplo.com.br"],
  "autoCreateUser": false,
  "status": "ACTIVE"
}
CampoTipoObrigatórioDescrição
namestring (1–255)SimNome de exibição, aparece na tela de login
providerTypeGOOGLE | MICROSOFT | GITHUB | METASimÚnico por organização
credentialsobject de stringSimEspera client_id e client_secret. Cifrado antes de gravar
redirectUristring (URL)SimA URI registrada no provedor. É o que vai no redirect_uri da chamada ao provedor
scopesstring[]NãoVazio usa o padrão do provedor (ver §8)
allowedDomainsstring[]NãoVazio aceita qualquer domínio
autoCreateUserbooleanNãoPadrão false. Hoje só é transportado na asserção — ver §15
statusACTIVE | INACTIVENãoPadrão ACTIVE
settingsobjectNãoAjustes por provedor. No MICROSOFT, settings.tenant

Resposta 201

json
{
  "data": {
    "type": "sso-provider-config",
    "id": "a3f1c8e2-0000-0000-0000-000000000001",
    "links": { "self": "/api/v1/sso/providers/a3f1c8e2-0000-0000-0000-000000000001" },
    "attributes": {
      "name": "Google Workspace da Financeira Exemplo",
      "providerType": "GOOGLE",
      "scopes": ["openid", "email", "profile"],
      "redirectUri": "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/auth/callback",
      "allowedDomains": ["financeira-exemplo.com.br"],
      "autoCreateUser": false,
      "status": "ACTIVE",
      "settings": null,
      "createdAt": "2026-08-17T12:00:00.000Z",
      "updatedAt": "2026-08-17T12:00:00.000Z"
    }
  }
}
{
  "data": {
    "type": "sso-provider-config",
    "id": "a3f1c8e2-0000-0000-0000-000000000001",
    "links": { "self": "/api/v1/sso/providers/a3f1c8e2-0000-0000-0000-000000000001" },
    "attributes": {
      "name": "Google Workspace da Financeira Exemplo",
      "providerType": "GOOGLE",
      "scopes": ["openid", "email", "profile"],
      "redirectUri": "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/auth/callback",
      "allowedDomains": ["financeira-exemplo.com.br"],
      "autoCreateUser": false,
      "status": "ACTIVE",
      "settings": null,
      "createdAt": "2026-08-17T12:00:00.000Z",
      "updatedAt": "2026-08-17T12:00:00.000Z"
    }
  }
}

Atenção. credentials não aparece na resposta, e não existe rota que o devolva. Perdeu o client_secret, gere outro no provedor e faça PATCH.

ErroQuando
400 VALIDATIONproviderType fora do enum, redirectUri que não é URL, campo obrigatório ausente
403Token sem organizationId, ou sem SSO_ADMIN
409 CONFLICTJá existe configuração desse providerType para a organização

GET /sso/api/v1/sso/providers/available

Rota pública, feita para a tela de login montar os botões antes de existir usuário autenticado. Exige organization_id na query.

bash
curl -s "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers/available?organization_id=$ORG_ID"
curl -s "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers/available?organization_id=$ORG_ID"
json
{ "data": [ { "providerType": "GOOGLE", "name": "Google Workspace da Financeira Exemplo" } ] }
{ "data": [ { "providerType": "GOOGLE", "name": "Google Workspace da Financeira Exemplo" } ] }

Devolve só configurações ACTIVE, e só providerType e name — nada de credencial, escopo ou domínio. Sem organization_id, responde 400.

GET /sso/api/v1/sso/auth/:provider/authorize

Inicia o fluxo. O :provider é convertido para maiúsculas antes da busca, então google e GOOGLE funcionam.

Parâmetro de queryTipoObrigatórioDescrição
organization_idUUIDSimQual organização, e portanto qual configuração usar
redirect_uriURLSimPara onde o callback devolve o navegador com a asserção

Resposta 200

json
{
  "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=...",
  "state": "6f1c...<64 hex>"
}
{
  "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=...",
  "state": "6f1c...<64 hex>"
}
ErroQuando
400 VALIDATIONorganization_id não é UUID, ou redirect_uri não é URL
400 BAD_REQUESTA configuração existe mas está INACTIVE
404 NOT_FOUNDA organização não tem configuração para esse provedor

GET /sso/api/v1/sso/auth/callback

Chamado pelo provedor, não pela sua aplicação. Em caso de sucesso responde 302 para o redirect_uri guardado no state, com a asserção em query string:

texto
302 Location: https://app.exemplo.com.br/auth/sso?sso_assertion=eyJhbGciOiJIUzI1NiJ9...
302 Location: https://app.exemplo.com.br/auth/sso?sso_assertion=eyJhbGciOiJIUzI1NiJ9...

Claims da asserção

ClaimDescrição
typeSempre sso_assertion. O authMiddleware recusa qualquer token que não seja access, então esta asserção não autentica em nenhum building block
organizationIdOrganização do fluxo
providerTypeQual provedor autenticou
identityIdID da SsoIdentity criada ou atualizada
externalIdIdentificador da pessoa dentro do provedor
email, emailVerifiedE-mail devolvido pelo provedor e se ele é verificado
displayName, avatarUrlOpcionais, quando o provedor os fornece
autoCreateUserCópia do sinalizador da configuração
subjectIdUsuário interno vinculado, quando já existe vínculo
ErroQuando
400 OAUTH_ERRORO provedor devolveu error na query
400 BAD_REQUESTFalta state ou code, ou o state expirou ou já foi usado
403 FORBIDDENO domínio do e-mail não está em allowedDomains
500 INTERNALA troca do código ou a busca de perfil no provedor falhou
json
{ "subjectId": "b1000000-0000-0000-0000-000000000001" }
{ "subjectId": "b1000000-0000-0000-0000-000000000001" }

Grava o subjectId na identidade. O serviço confere antes que a identidade pertence à organização do token, resolvendo pela providerConfigId; se não pertencer, responde 403. Não há verificação de que o subjectId existe no IAM — ver §15.


10

Início rápido

Do zero a uma URL de autorização válida. Cada passo mostra a resposta esperada.

Passo 1 — obter um token do IAM.

bash
TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@catalisa.app",
    "password": "root123456",
    "organizationId": "b0000000-0000-0000-0000-000000000001"
  }' | jq -r .accessToken)

ORG_ID="b0000000-0000-0000-0000-000000000001"
TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@catalisa.app",
    "password": "root123456",
    "organizationId": "b0000000-0000-0000-0000-000000000001"
  }' | jq -r .accessToken)

ORG_ID="b0000000-0000-0000-0000-000000000001"

O token precisa carregar SSO_ADMIN nas permissões. Se não carregar, o passo 2 responde 403 — ver §15, porque hoje isso exige configuração explícita.

Passo 2 — registrar o provedor.

bash
curl -s -X POST https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name": "Google de Teste",
    "providerType": "GOOGLE",
    "credentials": {
      "client_id": "SEU_CLIENT_ID.apps.googleusercontent.com",
      "client_secret": "SEU_CLIENT_SECRET"
    },
    "redirectUri": "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/auth/callback",
    "allowedDomains": ["catalisa.app"]
  }' | jq
curl -s -X POST https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name": "Google de Teste",
    "providerType": "GOOGLE",
    "credentials": {
      "client_id": "SEU_CLIENT_ID.apps.googleusercontent.com",
      "client_secret": "SEU_CLIENT_SECRET"
    },
    "redirectUri": "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/auth/callback",
    "allowedDomains": ["catalisa.app"]
  }' | jq

Esperado: 201, com o data.id da configuração e sem o campo credentials.

Passo 3 — conferir que as credenciais gravaram.

bash
CFG_ID=$(curl -s https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers \
  -H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id')

curl -s -X POST "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers/$CFG_ID/test" \
  -H "Authorization: Bearer $TOKEN" | jq
CFG_ID=$(curl -s https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers \
  -H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id')

curl -s -X POST "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers/$CFG_ID/test" \
  -H "Authorization: Bearer $TOKEN" | jq

Esperado: { "success": true, "message": "..." }. Isso confirma que a decifra funcionou e que o formato da credencial bate com o provedor.

Passo 4 — ver o botão que a tela de login mostraria.

bash
curl -s "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers/available?organization_id=$ORG_ID" | jq
curl -s "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers/available?organization_id=$ORG_ID" | jq

Esperado: { "data": [ { "providerType": "GOOGLE", "name": "Google de Teste" } ] }.

Passo 5 — gerar a URL de autorização.

bash
curl -s "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/auth/GOOGLE/authorize?organization_id=$ORG_ID&redirect_uri=https%3A%2F%2Fexemplo.catalisa.app%2Fauth%2Fsso" | jq
curl -s "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/auth/GOOGLE/authorize?organization_id=$ORG_ID&redirect_uri=https%3A%2F%2Fexemplo.catalisa.app%2Fauth%2Fsso" | jq

Esperado: um objeto com authorizationUrl apontando para accounts.google.com e um state de 64 caracteres hexadecimais. Abrir essa URL no navegador conclui o fluxo e devolve o navegador ao redirect_uri com ?sso_assertion=....

Credenciais de staging, conforme AMBIENTES.md. Nunca use credencial de produção nem client_secret real em documentação ou script de exemplo. Os comandos acima não foram executados contra staging na escrita deste documento — o SSO ainda não consta na lista de serviços publicados de staging.


11

Receitas

Habilitar login corporativo para uma empresa cliente

Objetivo. Ligar o Microsoft Entra ID de um cliente restringindo ao tenant e ao domínio dele.

bash
curl -s -X POST https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name": "Entra ID da Financeira Exemplo",
    "providerType": "MICROSOFT",
    "credentials": { "client_id": "...", "client_secret": "..." },
    "redirectUri": "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/auth/callback",
    "allowedDomains": ["financeira-exemplo.com.br"],
    "settings": { "tenant": "TENANT_ID_DO_CLIENTE" }
  }'
curl -s -X POST https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "name": "Entra ID da Financeira Exemplo",
    "providerType": "MICROSOFT",
    "credentials": { "client_id": "...", "client_secret": "..." },
    "redirectUri": "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/auth/callback",
    "allowedDomains": ["financeira-exemplo.com.br"],
    "settings": { "tenant": "TENANT_ID_DO_CLIENTE" }
  }'

Armadilhas.

  • Sem settings.tenant, o valor é common: qualquer tenant do Entra e qualquer conta pessoal Microsoft passam pelo provedor. A barreira que sobra é o allowedDomains. Use os dois.
  • O redirectUri tem que ser idêntico ao registrado no portal do provedor, incluindo esquema, host e caminho. Divergência de um caractere resulta em erro do lado do provedor, antes de a requisição chegar aqui.
  • Só existe uma configuração por (organização, providerType). A segunda tentativa devolve 409; para trocar as credenciais, use PATCH.

Trocar as credenciais de um provedor sem derrubar o login

Objetivo. Rotacionar o client_secret no provedor e refletir isso aqui.

bash
curl -s -X PATCH "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers/$CFG_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "credentials": { "client_id": "...", "client_secret": "NOVO_SEGREDO" } }'
curl -s -X PATCH "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers/$CFG_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "credentials": { "client_id": "...", "client_secret": "NOVO_SEGREDO" } }'

Armadilhas.

  • O PATCH substitui o objeto credentials inteiro. Mandar só o client_secret apaga o client_id. Envie sempre os dois.
  • Não há período de graça: a troca vale na próxima autenticação. Gere o segredo novo no provedor, confirme que o antigo ainda vale lá, faça o PATCH e só então revogue o antigo no provedor.
  • Depois do PATCH, rode o POST /providers/:id/test antes de considerar concluído.

Desligar um provedor temporariamente

Objetivo. Parar o login por um provedor sem perder a configuração nem as identidades.

bash
curl -s -X PATCH "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers/$CFG_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "status": "INACTIVE" }'
curl -s -X PATCH "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers/$CFG_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "status": "INACTIVE" }'

O provedor some de /providers/available e o /authorize passa a responder 400. As SsoIdentity continuam de pé; voltar para ACTIVE restaura tudo.

Armadilha. DELETE não é o caminho para isso. Ele faz exclusão lógica da configuração e as identidades ficam apontando para uma configuração excluída, o que deixa a listagem de identidades daquela organização inconsistente.

Reconciliar identidades órfãs depois de ligar o SSO numa base existente

Objetivo. Encontrar quem já autenticou pelo provedor mas ainda não está ligado a um usuário interno.

bash
# 1. Listar as identidades da organização
curl -s "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/identities?pageSize=100" \
  -H "Authorization: Bearer $TOKEN" | jq '.data[] | select(.attributes.subjectId == null) | {id, email: .attributes.email}'

# 2. Para cada uma, achar o usuário interno pelo e-mail e vincular
curl -s -X PUT "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/identities/$IDENT_ID/link" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "subjectId": "'"$USER_ID"'" }'
# 1. Listar as identidades da organização
curl -s "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/identities?pageSize=100" \
  -H "Authorization: Bearer $TOKEN" | jq '.data[] | select(.attributes.subjectId == null) | {id, email: .attributes.email}'

# 2. Para cada uma, achar o usuário interno pelo e-mail e vincular
curl -s -X PUT "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/identities/$IDENT_ID/link" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "subjectId": "'"$USER_ID"'" }'

Armadilhas.

  • O PUT .../link não verifica se o subjectId existe no IAM. Um UUID digitado errado é aceito e vira vínculo apontando para nada. Sempre pegue o ID de uma consulta ao IAM, nunca digite.
  • Não há verificação de unicidade: duas identidades diferentes podem apontar para o mesmo subjectId. Isso é útil (a mesma pessoa com Google e Microsoft) e perigoso (colagem errada). Confira antes.
  • A busca aceita filter por email, subjectId e providerType na query, o que ajuda em bases grandes.

Diagnosticar um login que falhou

SintomaCausa provávelO que fazer
404 no /authorizeA organização não tem configuração para esse provedorGET /providers e confira o providerType
400 BAD_REQUEST no /authorizeConfiguração INACTIVEPATCH com status: "ACTIVE"
400 no callback com "Invalid or expired SSO state"Passou de SSO_STATE_TTL (600s) ou o state já foi usadoReinicie o fluxo pelo /authorize
403 no callbackDomínio do e-mail fora de allowedDomainsConfira o campo na configuração
500 no callbackFalha na troca do código ou na busca de perfil no provedorVerifique redirectUri idêntico ao do provedor e o client_secret
Erro de decifra ao iniciar o fluxoSSO_CREDENTIAL_MASTER_KEY mudou depois de gravar a configuraçãoRegrave as credenciais com PATCH usando a chave atual

12

Integração com outros building blocks

Building blockComo se relacionaObrigatório
IAMEmite o token que protege as rotas administrativas do SSO; define organizationId e as permissões SSO_*. É quem deve, no fim, emitir o access token da sessãoSim
Audit TrailConsome o evento sso.login.success publicado a cada autenticação bem-sucedidaNão
API KeysCaminho paralelo, para máquina em vez de pessoaNão
Webhooks EnginePode entregar sso.login.success a sistemas externosNão

Qual dos três eu uso?

Esta é a pergunta que o IAM, o SSO e o API Keys respondem juntos. A régua é quem está do outro lado.

flowchart TD
  Q1{"Quem está autenticando?"}
  Q1 -->|"Uma pessoa"| Q2{"Ela já tem conta<br/>em Google, Microsoft,<br/>GitHub ou Meta que<br/>você quer reusar?"}
  Q1 -->|"Um sistema"| Q3{"A integração roda<br/>continuamente e precisa<br/>de escopo dinâmico?"}
  Q2 -->|sim| SSO["**SSO**<br/>fluxo federado → asserção"]
  Q2 -->|não| IAM["**IAM**<br/>e-mail e senha → access token"]
  Q3 -->|sim| IAM2["**IAM**<br/>client_credentials → token de 1h"]
  Q3 -->|não| AK["**API Keys**<br/>credencial estática de longa duração"]
  SSO -.->|"a sessão vira token do IAM"| IAM
Você querUseCredencial
Pessoa entrando com a conta corporativa delaSSOAsserção de curta duração, trocada por sessão
Pessoa entrando com e-mail e senha da plataformaIAMAccess token de 1 hora + refresh token
Sistema com integração viva e escopo variávelIAM (client_credentials)client_id + client_secret rotacionáveis
Sistema com integração simples e credencial fixaAPI KeysChave prefixo.segredo de longa duração

Onde o SSO entra na cadeia

flowchart LR
  IdP["Provedor externo<br/>Google · Microsoft · GitHub · Meta"] --> SSO["SSO<br/>valida, cria SsoIdentity,<br/>assina a asserção"]
  SSO -->|"asserção (type sso_assertion)"| APP["Sua aplicação"]
  APP -->|"identifica o usuário interno"| IAM["IAM"]
  IAM -->|"access token (type access)"| BBS["Os demais building blocks"]
  SSO -.->|"evento sso.login.success"| AT["Audit Trail"]

O ponto comercial é o encaixe: o SSO não cria um segundo universo de identidade. Ele termina o trabalho dele entregando uma prova assinada, e quem manda no acesso continua sendo o IAM — o mesmo token, o mesmo vocabulário de permissões, os mesmos middlewares em todos os building blocks.

Atenção. A seta tracejada entre a asserção e o token do IAM é hoje responsabilidade da aplicação: o IAM não expõe endpoint que troque a asserção por access token. Veja a §15.


13

Configuração e operação

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
SSO_CREDENTIAL_MASTER_KEYChave AES-256-GCM das credenciais de provedor. 64 caracteres hexadecimais (32 bytes). Gere com openssl rand -hex 32Sim, para usar o módulo—
SSO_STATE_TTLVida do state no Redis, em segundos. É o tempo que o usuário tem para concluir o login no provedorNão600
SSO_ASSERTION_TTLVida da asserção assinada, em segundosNão300
JWT_SECRETSegredo HS256 usado para assinar a asserção. O mesmo do IAM, mínimo 44 caracteresSim—
DATABASE_URLPostgreSQL, schema ssoSim—
REDIS_URLRedis, usado para o stateSim—
MODULE_SSO_URLURL do módulo em modo standalone, para os demais o alcançaremNão''
PORTPorta em standaloneNão3022 conforme DEFAULT_MODULE_PORTS
DEPLOYMENT_MODEmonolith ou standaloneNãomonolith

Atenção. SSO_CREDENTIAL_MASTER_KEY é validada por formato — 64 caracteres hexadecimais — e o módulo lança erro ao cifrar ou decifrar se ela estiver ausente ou malformada. Trocar essa chave torna ilegíveis todas as credenciais já gravadas: não há reencriptação automática. Trocar exige regravar cada configuração com PATCH.

Dependências de infraestrutura

DependênciaPara quêSe cair
PostgreSQL (schema sso)Configurações e identidadesNada funciona
Redisstate do fluxo OAuthNinguém consegue iniciar nem concluir login
IAMToken e permissões das rotas administrativasRotas administrativas respondem 401; o fluxo público continua
Provedores externosAutorização e perfilO provedor afetado para; os outros seguem

Limites e quotas

LimiteValorOnde
Configurações por organizaçãoUma por providerType, ou seja, até 4Único (organizationId, providerType)
Tamanho do corpo da requisição1 MBapplyCommonMiddleware
Paginação de identidadespageSize máximo 100, padrão 20listIdentitiesQuerySchema
Janela para concluir o loginSSO_STATE_TTL, 600s por padrãoRedis
Vida da asserçãoSSO_ASSERTION_TTL, 300s por padrãoJWT

Catálogo de erros

CódigoSignificaO que fazer
VALIDATION (400)Corpo ou query fora do schema ZodConfira tipos e enums em §9
BAD_REQUEST (400)Provedor INACTIVE, state inválido ou expirado, organization_id ausenteReinicie o fluxo ou ative o provedor
OAUTH_ERROR (400)O provedor externo devolveu error no callbackA mensagem vem do provedor; consulte o painel dele
FORBIDDEN (403)Domínio fora de allowedDomains, token sem organizationId, permissão ausente, ou identidade de outra organizaçãoConfira o allowedDomains e as permissões do token
NOT_FOUND (404)Configuração ou identidade inexistente na organizaçãoListe antes de buscar por ID
CONFLICT (409)Já existe configuração desse provedor na organizaçãoUse PATCH
INTERNAL (500)Falha ao gravar o state no Redis, ao chamar o provedor ou ao assinar a asserçãoVerifique Redis e a conectividade com o provedor

Observabilidade

  • Evento sso.login.success é publicado a cada autenticação concluída, com organizationId, providerType, identityId e email. É a métrica primária de uso e a matéria-prima do Audit Trail. A publicação é fire and forget: falha em publicar não derruba o login.
  • lastLoginAt na SsoIdentity responde "quem ainda usa este provedor" sem depender de log.
  • Chaves sso:state:* no Redis dão a contagem de fluxos em andamento. Volume alto com poucos eventos de sucesso indica gente começando o login e não terminando.
  • Não há evento de falha. Login recusado por domínio ou por state expirado não gera evento — só a resposta HTTP.

14

Segurança e compliance

Isolamento entre tenants — especificamente, no código.

flowchart TD
  R["Requisição"] --> A["authMiddleware<br/>assinatura HS256 e type = access"]
  A -->|falha| E401["401"]
  A --> P["requirePermission(SSO_ADMIN, SSO_READ, ...)"]
  P -->|falha| E403["403"]
  P --> O["requireOrganization<br/>claim organizationId presente?"]
  O -->|ausente| E403
  O --> S["Service recebe organizationId do token"]
  S --> Q["Repositório filtra por organizationId<br/>em toda consulta"]
  Q --> D["Só dados desta organização"]

O organizationId chega por claim assinado e é passado explicitamente para cada método de serviço — list(organizationId), getById(id, organizationId), findById(id, organizationId). Nenhuma rota lê organização do corpo da requisição.

As identidades são um caso especial e vale explicar: a SsoIdentity não guarda organizationId. O isolamento é indireto, pela SsoProviderConfig. Toda leitura de identidade passa por SsoIdentityService.getById, que carrega a configuração com o organizationId do token e responde 403 se ela não pertencer àquela organização. linkToSubject, unlinkFromSubject e delete chamam esse mesmo getById antes de agir, então as quatro operações herdam a verificação.

Dados sensíveis e como são tratados

DadoTratamento
client_id e client_secret do provedorCifrados com AES-256-GCM (IV de 12 bytes por registro, tag de autenticação), no formato iv:authTag:ciphertext. A chave é SSO_CREDENTIAL_MASTER_KEY, variável de ambiente, nunca no banco
Credenciais em resposta de APINunca devolvidas. O serializador expõe uma lista fixa de atributos que não inclui credentials
E-mail e nome do usuário externoGravados em claro em sso_identities — são necessários para a reconciliação com o usuário interno
Perfil bruto do provedorGuardado em rawProfile (JSON). Contém o que o provedor devolveu, incluindo avatar e identificadores
state e code_verifier (PKCE)Só no Redis, com TTL, apagados na leitura
Asserção assinadaJWT HS256 com type: 'sso_assertion', vida de 300s por padrão
Tokens do provedorO access_token do provedor é usado para buscar o perfil e não é persistido

Autenticação e permissões exigidas

PermissãoConcede
SSO_ADMINCriar, atualizar, excluir e testar configuração de provedor
SSO_READListar e ler configurações (sem credenciais)
SSO_IDENTITIES_READListar e ler identidades
SSO_IDENTITIES_MANAGEVincular, desvincular e excluir identidades

Três rotas são públicas por necessidade: /providers/available (a tela de login precisa dela antes de existir sessão) e as duas do fluxo de autenticação. As três passam pelo rate limit global e pelos cabeçalhos de segurança do applyCommonMiddleware.

Enquadramento regulatório

  • LGPD. E-mail, nome e avatar do usuário externo são dado pessoal, e rawProfile pode conter mais do que a plataforma precisa. A base legal é a execução do contrato com a empresa cliente, que é a controladora. Atender a pedido de eliminação é DELETE /sso/api/v1/sso/identities/:id, que remove a linha de fato — não é exclusão lógica.
  • Minimização. scopes é configurável por organização justamente para pedir ao provedor só o que se vai usar. Escopo pedido é dado recebido, e dado recebido é dado guardado em rawProfile.
  • Retenção. Não há expurgo automático de identidades inativas. Quem precisa de política de retenção implementa por fora, com base em lastLoginAt.

Boas práticas de operação

Atenção. A asserção viaja em query string, e query string entra em log de servidor web, de proxy reverso e no histórico do navegador. Consuma a asserção imediatamente ao receber, troque-a por sessão própria e não a registre em log. O TTL curto existe para limitar essa exposição, não para eliminá-la.

  • Guarde a SSO_CREDENTIAL_MASTER_KEY com SOPS, como as demais master keys da plataforma — ver SECRETS-SOPS-REFERENCE.md.
  • Preencha allowedDomains sempre que a organização for corporativa. É a diferença entre "quem tem conta Google" e "quem trabalha na empresa cliente".
  • No MICROSOFT, combine settings.tenant com allowedDomains. Só o segundo deixa passar conta de outro tenant até a checagem de domínio.

15

Limitações conhecidas

Esta é a seção que decide se o SSO serve para o seu caso. Leia antes de prometer prazo.

O ciclo não fecha sozinho: falta a troca da asserção por token

O callback devolve uma asserção assinada, e é aí que o building block termina. Não existe endpoint no IAM que receba essa asserção e emita um access token. Nenhum código fora de src/sso/ consome o tipo sso_assertion hoje. Quem integra precisa validar a asserção na própria aplicação — a assinatura é HS256 com o JWT_SECRET da plataforma — e decidir como criar a sessão. É a razão principal de o status ser beta e não produção.

autoCreateUser é transportado, não executado

O sinalizador existe na configuração e viaja na asserção, mas nenhum código cria usuário a partir dele. Provisionamento automático é trabalho da aplicação. O campo existe para que, quando o passo anterior for implementado, a intenção já esteja registrada por organização.

Nenhum papel concede as permissões SSO_*

SSO_ADMIN, SSO_READ, SSO_IDENTITIES_READ e SSO_IDENTITIES_MANAGE existem no vocabulário de permissões do IAM, mas não aparecem no mapa ROLE_PERMISSIONS de nenhum papel. Na prática, um usuário com papel padrão recebe 403 em todas as rotas administrativas do SSO. Habilitar exige tratar isso na configuração de papéis e permissões da organização.

Sem SAML 2.0 e sem SCIM

Só há OAuth 2.0 e OpenID Connect. Cliente corporativo que exige SAML — o caso comum com ADFS, Okta ou Ping como IdP — não é atendido, e não há contorno. Provisionamento e desprovisionamento automático de usuários por SCIM também não existe: desligar alguém no provedor impede o próximo login, mas não remove nem desativa nada na plataforma.

Quatro provedores, e só

GOOGLE, MICROSOFT, GITHUB e META. Não há Apple, LinkedIn, Slack, Okta nem provedor OIDC genérico. Adicionar um exige implementar IOAuthProvider, registrar na fábrica e adicionar valor ao enum do Prisma — o que é migração de banco, não configuração.

Sem portal para o cliente final se configurar

Toda configuração passa pela API, com SSO_ADMIN. Não há tela em que o time de TI da empresa cliente registre as próprias credenciais. É diferença relevante contra o WorkOS, cujo admin portal é argumento de venda.

Sem refresh do token do provedor

O access_token do provedor é usado uma vez, para buscar o perfil, e descartado. O refresh_token, quando o provedor manda, não é guardado. O SSO não consegue consultar a API do provedor depois do login — não dá para ler o calendário, os grupos ou o diretório do usuário.

O vínculo com o usuário interno não é validado

PUT /identities/:id/link aceita qualquer UUID como subjectId sem consultar o IAM. Não há verificação de existência nem de unicidade: dois vínculos podem apontar para o mesmo usuário interno. É desejável no caso de a mesma pessoa ter Google e Microsoft, e é armadilha no caso de erro de digitação.

Exclusão de configuração deixa identidades pendentes

DELETE /providers/:id é exclusão lógica, mas as SsoIdentity continuam apontando para a configuração excluída, sem cascata nem marcação. Para desligar um provedor, prefira status: "INACTIVE".

Sem eventos de falha e sem métrica de latência do provedor

Só sso.login.success é publicado. Login recusado, state expirado e falha no provedor não geram evento. Diagnóstico de queda em taxa de sucesso depende de log e da resposta HTTP.


16

Perguntas frequentes

Quais provedores de identidade estão de fato implementados?

Quatro: Google, Microsoft (Entra ID), GitHub e Meta. Esses são os valores do enum SsoProviderType, e para cada um existe uma classe em src/sso/providers/. Não há SAML, não há OIDC genérico e não há Apple. Se a resposta que você precisa dar a um cliente é "sim, temos SSO", vale conferir antes qual protocolo ele está pedindo.

Qual a diferença entre o SSO e o IAM?

O IAM é dono da identidade dentro da plataforma: ele guarda o usuário, a organização, o papel, e emite o token que todos os building blocks verificam. O SSO não guarda usuário nem emite token de acesso — ele conversa com um provedor externo, prova que a pessoa autenticou lá e devolve essa prova assinada. Os dois se encaixam: o SSO termina onde o IAM começa.

Posso usar a asserção do SSO como token de acesso nas APIs?

Não. A asserção carrega type: 'sso_assertion', e o authMiddleware de todos os building blocks recusa qualquer token cujo type não seja access. Isso é proposital: a asserção prova autenticação, não concede autorização.

O SSO substitui o login por senha?

Ele convive. Uma organização pode ter Google ativo e usuários entrando por e-mail e senha ao mesmo tempo. A tela de login mostra os botões de GET /providers/available ao lado do formulário tradicional. Não há como desligar o login por senha pelo SSO — isso é decisão do IAM.

Como restrinjo o login ao domínio da empresa cliente?

Preencha allowedDomains na configuração. O callback compara o domínio do e-mail devolvido pelo provedor e responde 403 antes de gravar a identidade. No Microsoft, combine com settings.tenant — sem ele o valor é common, que aceita conta de qualquer tenant e conta pessoal.

O que acontece se eu trocar a SSO_CREDENTIAL_MASTER_KEY?

Todas as credenciais já gravadas ficam ilegíveis, e qualquer tentativa de iniciar o fluxo daquele provedor falha na decifra. Não existe reencriptação automática. Se precisar trocar, planeje regravar cada configuração com PATCH usando a chave nova.

Uma empresa cliente pode ter Google e Microsoft ao mesmo tempo?

Pode. A restrição é uma configuração por (organização, providerType), então dá para ter os quatro provedores ativos na mesma organização. A mesma pessoa que entrar pelos dois vai gerar duas SsoIdentity distintas — vincule as duas ao mesmo subjectId.

Por que o segmento sso aparece duas vezes na URL?

Porque o basePath do módulo é /sso e os routers são montados em /api/v1/sso/.... O resultado é /sso/api/v1/sso/providers. É feio e é o comportamento real — as rotas da §9 estão escritas como respondem, não como deveriam ser.

Dá para adicionar um provedor OIDC genérico?

Hoje não sem mexer no código. Seriam três passos: implementar a interface IOAuthProvider, registrar na fábrica createOAuthProvider e adicionar o valor ao enum SsoProviderType do Prisma — este último é migração de banco. Não é configuração.

O SSO funciona sem o Redis?

Não. O state do fluxo OAuth vive exclusivamente no Redis, com TTL. Sem Redis, nenhum login federado começa nem termina. As rotas administrativas continuam funcionando, porque só dependem do PostgreSQL.