SSO
BetaLogin com Google, Microsoft, GitHub e Meta para as empresas que você atende
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.
- 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
- 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
- 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
15 endpoints em 4 recursos.
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.
| Atributo | Valor |
|---|---|
| Identificador | sso |
| Categoria | Identidade |
| Escopo | Tenant (exige organizationId no token nas rotas administrativas) |
| Porta (standalone) | 3022 |
| Path alias | @sso |
| Prefixo HTTP | /sso |
| Status | Beta desde 2026-02 |
| Depende de | PostgreSQL, Redis, IAM |
O problema
negócioO 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_verifiedno 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_secretda 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.
statesem 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.brentra" 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.
Proposta de valor
negócio| Antes | Depois |
|---|---|
| Uma integração OAuth escrita à mão por provedor | Quatro 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 puro | Cifrado com AES-256-GCM no banco, nunca devolvido em resposta |
| Habilitar provedor para um cliente exige deploy | POST /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 hoc | Campo allowedDomains na configuração, verificado no callback |
| Custo por conexão que cresce com a base de clientes | Custo 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"]
endQuatro 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.
Casos de uso reais
negócioCaso 1 — Uma financeira exige que o correspondente entre pelo Google Workspace da empresa dele Cenário ilustrativo
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.
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 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çãoO 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
Empresa cliente com 200 usuários já cadastrados na plataforma que decide ligar o Microsoft Entra ID.
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.
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
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
Plataforma B2B com 40 empresas clientes, das quais 18 pedem login corporativo.
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.
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álculo | Conexão ativa por mês | Configuração no banco |
| 18 conexões | 18 × preço de tabela do fornecedor | Mesmo custo de infraestrutura |
| Custo marginal da 19ª | Mais uma conexão cobrada | Uma linha no banco |
Caso 4 — O sobrepreço de SSO é um problema documentado publicamente Referência de mercado
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 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.
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 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.
Mercado e diferenciais
negócioPanorama. 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ério | Catalisa SSO | WorkOS | Auth0 | Keycloak |
|---|---|---|---|---|
| Modelo de preço | Incluso na plataforma | US$ 125/conexão/mês na 1ª faixa | Por usuário ativo + US$ 100/conexão adicional | Licença zero, você opera |
| OAuth 2.0 / OIDC | Sim, 4 provedores | Sim | Sim, catálogo amplo | Sim |
| SAML 2.0 | Não (ver §15) | Sim | Sim | Sim |
| SCIM / Directory Sync | Não (ver §15) | Sim | Sim | Parcial |
| Configuração por organização | Sim, no banco | Sim, via admin portal | Só nos planos B2B | Via realm, um por cliente |
| Restrição por domínio de e-mail | Sim, allowedDomains | Sim | Sim | Sim |
| Credenciais do cliente cifradas em repouso | Sim, AES-256-GCM | Gerenciado pelo fornecedor | Gerenciado pelo fornecedor | Depende da sua configuração |
| Portal para o cliente final se configurar | Não (ver §15) | Sim | Parcial | Sim, console admin |
| Operação por sua conta | Já vem operada | Não | Não | Sim, 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
- 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.
- 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.
- 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 nenhumclient_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 é | Escolha | Por quê |
|---|---|---|
| Cliente corporativo que exige SAML 2.0, ADFS ou Okta como IdP | WorkOS ou Keycloak | O SSO da Catalisa não implementa SAML, e isso não tem contorno |
| Provisionamento automático de usuários por SCIM | WorkOS | Não implementamos Directory Sync |
| O time de TI do cliente quer configurar a conexão sozinho, num portal | WorkOS | Não temos portal self-service; a configuração passa por quem tem SSO_ADMIN |
| Cobertura máxima de protocolo, com equipe para operar | Keycloak | OIDC, 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 Catalisa | Catalisa SSO | É exatamente o recorte implementado |
Modelo de cobrança e ROI
negócioUnidade 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:
| Driver | Por que importa | Ordem de grandeza |
|---|---|---|
| Organizações com provedor ativo | Uma linha em SsoProviderConfig por organização e provedor | Custo de armazenamento desprezível |
| Autenticações federadas por mês | Cada login gasta uma chave no Redis por até SSO_STATE_TTL e faz 2 chamadas HTTP ao provedor | Latência dominada pelo provedor externo |
Comparação de custo — cenário: plataforma B2B com 40 empresas clientes, das quais 18 pedem login corporativo.
| Catalisa SSO | WorkOS | Auth0 (B2B Essentials) | |
|---|---|---|---|
| Base de cálculo | Incluso na plataforma | Por conexão de SSO por mês | Por 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 30 | 3 conexões inclusas; US$ 100/mês por conexão adicional |
| 18 conexões | Sem custo adicional por conexão | 18 conexões na faixa de US$ 100 cada | 3 inclusas + 15 adicionais |
| Custo marginal da 19ª empresa | Uma linha no banco | Mais uma conexão cobrada | Mais 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.
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<T, AppError>"| 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
statevive no Redis, não em cookie. Um cookie exigiria que o/authorizee o/callbackcompartilhassem 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. OhandleCallbacklê 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,MICROSOFTeMETArecebemcode_challenge; oGITHUBnã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: oauthMiddlewareda plataforma recusa qualquer token cujotypenão sejaaccess, 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_TTLsegundos — 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.
createOAuthProviderdevolveResult<IOAuthProvider, AppError>: umproviderTypedesconhecido 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.
Conceitos e modelo de dados
Glossário
| Termo | Significa |
|---|---|
| Provider config | A configuração de um provedor para uma organização. Guarda credenciais cifradas, escopos, URI de retorno e domínios permitidos. |
| Provider type | Qual provedor externo: GOOGLE, MICROSOFT, GITHUB ou META. Não há outros implementados. |
| Identity | A identidade de uma pessoa naquele provedor, naquela configuração. Chave natural: providerConfigId + externalId. |
| External ID | O identificador da pessoa dentro do provedor. É o sub do Google, o id do GitHub — nunca o e-mail. |
| Subject | O usuário interno da plataforma ao qual a identidade externa foi vinculada. Guardado em subjectId, nulo até alguém vincular. |
| State | Valor aleatório de 32 bytes que amarra o /authorize ao /callback e protege contra CSRF de login. Vive no Redis, uso único. |
| PKCE | Proof Key for Code Exchange (RFC 7636). O code_verifier fica no state; o code_challenge vai ao provedor. |
| Asserção SSO | JWT de curta duração, assinado em HS256, que o callback devolve provando quem autenticou. Não é access token. |
| Allowed domains | Lista de domínios de e-mail aceitos. Vazia significa qualquer domínio. |
| Auto create user | Sinalizador 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 Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
SsoProviderConfig | sso.sso_provider_configs | Configuração de um provedor para uma organização | organizationId, providerType, credentials (cifrado), redirectUri, allowedDomains, autoCreateUser, status, settings, deletedAt. Único (organizationId, providerType) |
SsoIdentity | sso.sso_identities | Identidade de uma pessoa em um provedor | providerConfigId, externalId, email, emailVerified, subjectId, rawProfile, lastLoginAt. Único (providerConfigId, externalId) |
Enumerações
| Enum | Valores | Observação |
|---|---|---|
SsoProviderType | GOOGLE · MICROSOFT · GITHUB · META | O enum do Prisma e a fábrica de provedores têm exatamente esses quatro |
SsoProviderConfigStatus | ACTIVE · INACTIVE | Só 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 noteO 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.
| Provedor | Escopos padrão | PKCE | Origem do emailVerified |
|---|---|---|---|
GOOGLE | openid, email, profile | Sim (S256) | Campo email_verified do userinfo |
MICROSOFT | openid, email, profile, User.Read | Sim (S256) | Fixo em true — o Microsoft Graph só devolve e-mail verificado |
GITHUB | read:user, user:email | Não | Campo verified do e-mail primário, via /user/emails |
META | email, public_profile | Sim (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.
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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /sso/api/v1/sso/providers | Cria a configuração de um provedor para a organização | SSO_ADMIN |
GET | /sso/api/v1/sso/providers | Lista as configurações da organização | SSO_READ |
GET | /sso/api/v1/sso/providers/available | Provedores ativos, para montar a tela de login | Pública |
GET | /sso/api/v1/sso/providers/:id | Busca uma configuração | SSO_READ |
PATCH | /sso/api/v1/sso/providers/:id | Atualiza a configuração | SSO_ADMIN |
DELETE | /sso/api/v1/sso/providers/:id | Exclusão lógica | SSO_ADMIN |
POST | /sso/api/v1/sso/providers/:id/test | Valida as credenciais gravadas | SSO_ADMIN |
Todas as rotas acima, exceto /available, exigem authMiddleware e requireOrganization — token sem organizationId recebe 403.
Identidades — /sso/api/v1/sso/identities
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /sso/api/v1/sso/identities | Lista identidades da organização, paginado | SSO_IDENTITIES_READ |
GET | /sso/api/v1/sso/identities/:id | Busca uma identidade | SSO_IDENTITIES_READ |
PUT | /sso/api/v1/sso/identities/:id/link | Vincula a identidade a um usuário interno | SSO_IDENTITIES_MANAGE |
DELETE | /sso/api/v1/sso/identities/:id/link | Desfaz o vínculo | SSO_IDENTITIES_MANAGE |
DELETE | /sso/api/v1/sso/identities/:id | Remove a identidade | SSO_IDENTITIES_MANAGE |
Todas exigem authMiddleware e requireOrganization.
Fluxo de autenticação — /sso/api/v1/sso/auth
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
GET | /sso/api/v1/sso/auth/:provider/authorize | Inicia o fluxo e devolve a URL de autorização | Pública |
GET | /sso/api/v1/sso/auth/callback | Recebe o retorno do provedor e redireciona com a asserção | Pública |
Saúde
| Método | Rota | Descrição |
|---|---|---|
GET | /sso/health | Sonda 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
{
"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"
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string (1–255) | Sim | Nome de exibição, aparece na tela de login |
providerType | GOOGLE | MICROSOFT | GITHUB | META | Sim | Único por organização |
credentials | object de string | Sim | Espera client_id e client_secret. Cifrado antes de gravar |
redirectUri | string (URL) | Sim | A URI registrada no provedor. É o que vai no redirect_uri da chamada ao provedor |
scopes | string[] | Não | Vazio usa o padrão do provedor (ver §8) |
allowedDomains | string[] | Não | Vazio aceita qualquer domínio |
autoCreateUser | boolean | Não | Padrão false. Hoje só é transportado na asserção — ver §15 |
status | ACTIVE | INACTIVE | Não | Padrão ACTIVE |
settings | object | Não | Ajustes por provedor. No MICROSOFT, settings.tenant |
Resposta 201
{
"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.
| Erro | Quando |
|---|---|
400 VALIDATION | providerType fora do enum, redirectUri que não é URL, campo obrigatório ausente |
403 | Token sem organizationId, ou sem SSO_ADMIN |
409 CONFLICT | Já 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.
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"{ "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 query | Tipo | Obrigatório | Descrição |
|---|---|---|---|
organization_id | UUID | Sim | Qual organização, e portanto qual configuração usar |
redirect_uri | URL | Sim | Para onde o callback devolve o navegador com a asserção |
Resposta 200
{
"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>"
}| Erro | Quando |
|---|---|
400 VALIDATION | organization_id não é UUID, ou redirect_uri não é URL |
400 BAD_REQUEST | A configuração existe mas está INACTIVE |
404 NOT_FOUND | A 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:
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
| Claim | Descrição |
|---|---|
type | Sempre sso_assertion. O authMiddleware recusa qualquer token que não seja access, então esta asserção não autentica em nenhum building block |
organizationId | Organização do fluxo |
providerType | Qual provedor autenticou |
identityId | ID da SsoIdentity criada ou atualizada |
externalId | Identificador da pessoa dentro do provedor |
email, emailVerified | E-mail devolvido pelo provedor e se ele é verificado |
displayName, avatarUrl | Opcionais, quando o provedor os fornece |
autoCreateUser | Cópia do sinalizador da configuração |
subjectId | Usuário interno vinculado, quando já existe vínculo |
| Erro | Quando |
|---|---|
400 OAUTH_ERROR | O provedor devolveu error na query |
400 BAD_REQUEST | Falta state ou code, ou o state expirou ou já foi usado |
403 FORBIDDEN | O domínio do e-mail não está em allowedDomains |
500 INTERNAL | A troca do código ou a busca de perfil no provedor falhou |
PUT /sso/api/v1/sso/identities/:id/link
{ "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.
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.
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.
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"]
}' | jqcurl -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"]
}' | jqEsperado: 201, com o data.id da configuração e sem o campo credentials.
Passo 3 — conferir que as credenciais gravaram.
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" | jqCFG_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" | jqEsperado: { "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.
curl -s "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers/available?organization_id=$ORG_ID" | jqcurl -s "https://sso.bb.stg.catalisa.app/sso/api/v1/sso/providers/available?organization_id=$ORG_ID" | jqEsperado: { "data": [ { "providerType": "GOOGLE", "name": "Google de Teste" } ] }.
Passo 5 — gerar a URL de autorização.
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" | jqcurl -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" | jqEsperado: 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_secretreal 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.
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.
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 é oallowedDomains. Use os dois. - O
redirectUritem 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 devolve409; para trocar as credenciais, usePATCH.
Trocar as credenciais de um provedor sem derrubar o login
Objetivo. Rotacionar o client_secret no provedor e refletir isso aqui.
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
PATCHsubstitui o objetocredentialsinteiro. Mandar só oclient_secretapaga oclient_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
PATCHe só então revogue o antigo no provedor. - Depois do
PATCH, rode oPOST /providers/:id/testantes de considerar concluído.
Desligar um provedor temporariamente
Objetivo. Parar o login por um provedor sem perder a configuração nem as identidades.
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.
# 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 .../linknão verifica se osubjectIdexiste 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
filterporemail,subjectIdeproviderTypena query, o que ajuda em bases grandes.
Diagnosticar um login que falhou
| Sintoma | Causa provável | O que fazer |
|---|---|---|
404 no /authorize | A organização não tem configuração para esse provedor | GET /providers e confira o providerType |
400 BAD_REQUEST no /authorize | Configuração INACTIVE | PATCH com status: "ACTIVE" |
400 no callback com "Invalid or expired SSO state" | Passou de SSO_STATE_TTL (600s) ou o state já foi usado | Reinicie o fluxo pelo /authorize |
403 no callback | Domínio do e-mail fora de allowedDomains | Confira o campo na configuração |
500 no callback | Falha na troca do código ou na busca de perfil no provedor | Verifique redirectUri idêntico ao do provedor e o client_secret |
| Erro de decifra ao iniciar o fluxo | SSO_CREDENTIAL_MASTER_KEY mudou depois de gravar a configuração | Regrave as credenciais com PATCH usando a chave atual |
Integração com outros building blocks
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite 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ão | Sim |
| Audit Trail | Consome o evento sso.login.success publicado a cada autenticação bem-sucedida | Não |
| API Keys | Caminho paralelo, para máquina em vez de pessoa | Não |
| Webhooks Engine | Pode entregar sso.login.success a sistemas externos | Nã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ê quer | Use | Credencial |
|---|---|---|
| Pessoa entrando com a conta corporativa dela | SSO | Asserção de curta duração, trocada por sessão |
| Pessoa entrando com e-mail e senha da plataforma | IAM | Access token de 1 hora + refresh token |
| Sistema com integração viva e escopo variável | IAM (client_credentials) | client_id + client_secret rotacionáveis |
| Sistema com integração simples e credencial fixa | API Keys | Chave 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.
Configuração e operação
Variáveis de ambiente
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
SSO_CREDENTIAL_MASTER_KEY | Chave AES-256-GCM das credenciais de provedor. 64 caracteres hexadecimais (32 bytes). Gere com openssl rand -hex 32 | Sim, para usar o módulo | — |
SSO_STATE_TTL | Vida do state no Redis, em segundos. É o tempo que o usuário tem para concluir o login no provedor | Não | 600 |
SSO_ASSERTION_TTL | Vida da asserção assinada, em segundos | Não | 300 |
JWT_SECRET | Segredo HS256 usado para assinar a asserção. O mesmo do IAM, mínimo 44 caracteres | Sim | — |
DATABASE_URL | PostgreSQL, schema sso | Sim | — |
REDIS_URL | Redis, usado para o state | Sim | — |
MODULE_SSO_URL | URL do módulo em modo standalone, para os demais o alcançarem | Não | '' |
PORT | Porta em standalone | Não | 3022 conforme DEFAULT_MODULE_PORTS |
DEPLOYMENT_MODE | monolith ou standalone | Não | monolith |
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ência | Para quê | Se cair |
|---|---|---|
PostgreSQL (schema sso) | Configurações e identidades | Nada funciona |
| Redis | state do fluxo OAuth | Ninguém consegue iniciar nem concluir login |
| IAM | Token e permissões das rotas administrativas | Rotas administrativas respondem 401; o fluxo público continua |
| Provedores externos | Autorização e perfil | O provedor afetado para; os outros seguem |
Limites e quotas
| Limite | Valor | Onde |
|---|---|---|
| Configurações por organização | Uma por providerType, ou seja, até 4 | Único (organizationId, providerType) |
| Tamanho do corpo da requisição | 1 MB | applyCommonMiddleware |
| Paginação de identidades | pageSize máximo 100, padrão 20 | listIdentitiesQuerySchema |
| Janela para concluir o login | SSO_STATE_TTL, 600s por padrão | Redis |
| Vida da asserção | SSO_ASSERTION_TTL, 300s por padrão | JWT |
Catálogo de erros
| Código | Significa | O que fazer |
|---|---|---|
VALIDATION (400) | Corpo ou query fora do schema Zod | Confira tipos e enums em §9 |
BAD_REQUEST (400) | Provedor INACTIVE, state inválido ou expirado, organization_id ausente | Reinicie o fluxo ou ative o provedor |
OAUTH_ERROR (400) | O provedor externo devolveu error no callback | A 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ção | Confira o allowedDomains e as permissões do token |
NOT_FOUND (404) | Configuração ou identidade inexistente na organização | Liste antes de buscar por ID |
CONFLICT (409) | Já existe configuração desse provedor na organização | Use PATCH |
INTERNAL (500) | Falha ao gravar o state no Redis, ao chamar o provedor ou ao assinar a asserção | Verifique Redis e a conectividade com o provedor |
Observabilidade
- Evento
sso.login.successé publicado a cada autenticação concluída, comorganizationId,providerType,identityIdeemail. É 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. lastLoginAtnaSsoIdentityresponde "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
stateexpirado não gera evento — só a resposta HTTP.
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
| Dado | Tratamento |
|---|---|
client_id e client_secret do provedor | Cifrados 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 API | Nunca devolvidas. O serializador expõe uma lista fixa de atributos que não inclui credentials |
| E-mail e nome do usuário externo | Gravados em claro em sso_identities — são necessários para a reconciliação com o usuário interno |
| Perfil bruto do provedor | Guardado 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 assinada | JWT HS256 com type: 'sso_assertion', vida de 300s por padrão |
| Tokens do provedor | O access_token do provedor é usado para buscar o perfil e não é persistido |
Autenticação e permissões exigidas
| Permissão | Concede |
|---|---|
SSO_ADMIN | Criar, atualizar, excluir e testar configuração de provedor |
SSO_READ | Listar e ler configurações (sem credenciais) |
SSO_IDENTITIES_READ | Listar e ler identidades |
SSO_IDENTITIES_MANAGE | Vincular, 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
rawProfilepode 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 emrawProfile. - 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_KEYcom SOPS, como as demais master keys da plataforma — ver SECRETS-SOPS-REFERENCE.md. - Preencha
allowedDomainssempre que a organização for corporativa. É a diferença entre "quem tem conta Google" e "quem trabalha na empresa cliente". - No
MICROSOFT, combinesettings.tenantcomallowedDomains. Só o segundo deixa passar conta de outro tenant até a checagem de domínio.
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.
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.