Login externo (SSO)
Use um provedor de identidade confiável no login de uma instalação do Nodaro, com asserção JWT assinada, OIDC ou SAML, e controle a vinculação de contas.
O login externo (SSO) permite que um provedor de identidade (IdP) confiável faça o login de usuários em uma instalação do Nodaro. Na integração principal, o seu IdP assina uma asserção JWT de curta duração, o Nodaro a verifica, e o navegador recebe uma sessão comum do Nodaro. O SSO fica desativado até que o operador da instalação configure pelo menos um provedor.
A conta em que o usuário entra é uma conta comum. O SSO não adiciona nenhuma credencial especial: ele é só uma forma de iniciar uma sessão.
Dois estilos de integração
| Tipo | Para | Como funciona |
|---|---|---|
assertion | Emissores que não falam OIDC nem SAML, como um host de incorporação que só consegue gerar um token assinado de curta duração | O seu IdP assina um JWT, e o Nodaro o verifica e o troca por uma sessão. Esse caminho funciona de ponta a ponta. |
oidc, saml | IdPs que falam OpenID Connect ou SAML padrão | A página de login inicia um login padrão com o Supabase Auth da própria instalação, que verifica a resposta do IdP. Esse caminho tem limites; veja Provedores OIDC e SAML. |
Configurar provedores
Defina EXTERNAL_SSO_PROVIDERS no servidor como um array JSON de provedores, direto no valor ou como @/path/to/providers.json (um @ no início lê o arquivo nesse caminho). Um valor malformado interrompe o servidor na inicialização, então um erro de digitação nunca descarta um provedor em silêncio nem deixa o login configurado pela metade.
[
{
"id": "acme-chat",
"label": "Acme Chat",
"kind": "assertion",
"secret": "a-dedicated-32-character-hmac-key-for-nodaro-only",
"audience": "nodaro",
"claimMap": { "email": "email", "emailVerified": "email_verified", "subject": "sub" },
"initiateUrl": "https://chat.example.com/oauth/nodaro",
"maxLifetimeSeconds": 300
},
{
"id": "keycloak",
"label": "Acme (Keycloak)",
"kind": "oidc",
"supabaseProvider": "keycloak"
}
]| Campo | Aplica-se a | Significado |
|---|---|---|
id | Todos | O slug do provedor, usado na rota /v1/sso/:id e guardado em cada conta vinculada. De 1 a 63 caracteres: primeiro uma letra minúscula ou um dígito, depois letras minúsculas, dígitos, _ e -. Sem pontos. Precisa ser único. |
label | Todos | O nome de exibição do provedor. |
kind | Todos | assertion, oidc ou saml. |
secret | assertion, obrigatório | A chave HS256 que verifica a assinatura da asserção, com pelo menos 16 caracteres. Use um segredo dedicado, nunca o segredo de assinatura de sessões do próprio IdP, para que um comprometimento do Nodaro não permita forjar sessões do IdP. |
audience | assertion, obrigatório | O valor a que a claim aud da asserção precisa ser igual. |
claimMap | assertion, opcional | Quais claims trazem o e-mail, o indicador de verificação e o subject. O padrão é email, email_verified e sub. |
initiateUrl | assertion, opcional | Para onde o botão de SSO envia o usuário. Sem ele, um clique no botão responde 400 no_assertion. |
initiateUrlByHost | assertion, opcional | Um mapa de um nome de host simples, em minúsculas e sem porta, para um endereço que substitui initiateUrl nesse host. Para uma implantação que responde em vários nomes de host. |
maxLifetimeSeconds | assertion, opcional | O maior tempo de vida (exp menos iat) que o Nodaro aceita. O padrão é 300 segundos, e o máximo é 3.600. |
domain | oidc, saml | Validado na inicialização, mas não usado. Veja Provedores OIDC e SAML. |
supabaseProvider | oidc | Validado na inicialização, mas não usado. |
initiateUrlnão repassa nada ao IdP. Se você quiser que o usuário chegue a uma página específica depois do login, o seu IdP adiciona&next=<path>ao redirecionar de volta.- As chaves de
initiateUrlByHostsão validadas na inicialização: uma chave com maiúsculas ou com porta interrompe o servidor. O mapa nunca é publicado, e a vinculação de contas não depende do host pelo qual o usuário chegou.
O contrato da asserção
O seu IdP gera um JWT e envia o navegador para GET /v1/sso/:provider?assertion=<jwt>. O Nodaro só aceita a asserção quando todas as regras são cumpridas:
- Algoritmo. O JWT é assinado com
HS256e com osecretdo provedor. - Audiência.
audé igual aoaudiencedo provedor. - Expiração.
expestá presente e não está no passado, com uma tolerância de 5 segundos para diferenças de relógio. - Tempo de vida.
expmenosiaté no máximomaxLifetimeSeconds, medido no relógio do Nodaro. Umiatmais de 5 segundos no futuro é limitado, e umiatausente conta como agora. - Uso único.
jtiestá presente e é único. O Nodaro lembra cadajtiaté passar a janela de validade da própria asserção e o recusa na segunda vez. - E-mail. A claim do e-mail está presente.
- E-mail verificado. O indicador de verificação é
truepara que uma conta seja criada ou vinculada. Um indicador ausente conta como não verificado.
Uma asserção que falha responde 401 com invalid_signature, invalid_claims, expired, too_long_lived, missing_jti, missing_email ou assertion_replayed. O endpoint aceita 20 requisições a cada 60 segundos por endereço IP; acima disso, ele responde 429 rate_limit_exceeded com um cabeçalho Retry-After.
Este exemplo do lado do IdP gera uma asserção válida com a biblioteca jose para Node.js:
import { SignJWT } from "jose"
import { randomUUID } from "node:crypto"
const secret = new TextEncoder().encode(process.env.NODARO_SSO_SECRET)
const assertion = await new SignJWT({ email: user.email, email_verified: true })
.setProtectedHeader({ alg: "HS256" })
.setSubject(user.id)
.setAudience("nodaro")
.setIssuedAt()
.setExpirationTime("2m")
.setJti(randomUUID())
.sign(secret)
const url = new URL("https://nodaro.example.com/v1/sso/acme-chat")
url.searchParams.set("assertion", assertion)
url.searchParams.set("next", "/projects")
res.redirect(url.toString())O fluxo de login
- O usuário clica no botão de SSO na página de login do Nodaro, que chama
GET /v1/sso/:providersem asserção. - O Nodaro redireciona (
302) para oinitiateUrldo provedor. - O IdP autentica o usuário e envia o navegador de volta para
GET /v1/sso/:provider?assertion=<jwt>, opcionalmente com&next=<path>. - O Nodaro verifica a asserção, recusa reutilizações e aplica as regras de vinculação de contas.
- O Nodaro redireciona para
/sso?sso_token=<one-time token>. Essa página troca o token de uso único por uma sessão. - O usuário chega a
/projectsou ao caminho denext.
Um IdP, como um host de incorporação, também pode começar pela etapa 3: ele redireciona o navegador para o endpoint de troca com uma asserção nova. O parâmetro ?redirect= da própria página de login não é levado pelos redirecionamentos do SSO; só um next adicionado pelo IdP é.
Mostrar o botão de SSO na página de login
A página de login mostra um botão de SSO quando as duas condições são cumpridas:
- O perfil de superfície da implantação lista
ssoemauth.methodse defineauth.ssoLabel. Um perfil semssoLabelremovessodos métodos. O texto do botão é ossoLabel. - Pelo menos um provedor está configurado. A página consulta
GET /v1/sso/providers.
Os perfis de superfície são um recurso das edições Business e Cloud. Veja Edições e perfis de superfície.
Um perfil cujo auth.methods lista apenas sso também ativa uma restrição no servidor. Toda conta com login precisa ter sido criada ou vinculada pelo SSO, e qualquer outra sessão é recusada na primeira chamada à API com 403 sso_required. A única exceção é a conta de cobrança da implantação, descrita abaixo. Adicionar email aos métodos desativa a restrição para toda a instalação.
Como as contas são vinculadas
Quando uma asserção é válida, o Nodaro encontra ou cria a conta seguindo regras feitas para que uma asserção nunca possa assumir uma conta existente que apenas compartilha o endereço de e-mail.
| Situação | Resultado |
|---|---|
| Nenhuma conta tem o e-mail, e o e-mail está verificado | Uma conta nova é criada e vinculada ao provedor. |
| Nenhuma conta tem o e-mail, e o e-mail não está verificado | Recusado com 403 email_unverified, para que uma claim não verificada não possa ocupar um endereço real. |
| A conta já está vinculada a este provedor | O usuário entra. |
| A conta está vinculada a outro provedor | Recusado com 403 account_linked_other_provider, mesmo quando EXTERNAL_SSO_LINK_EXISTING está ativado. |
Existe uma conta local sem SSO, EXTERNAL_SSO_LINK_EXISTING está ativado e o e-mail está verificado | A conta é vinculada ao provedor, e o usuário entra. |
| Existe uma conta local sem SSO, e a opção está desativada ou o e-mail não está verificado | Recusado com 403 account_exists. |
EXTERNAL_SSO_LINK_EXISTING vem desativado por padrão, que é a configuração segura contra a tomada de contas. Só true ou 1 o ativam, com qualquer combinação de maiúsculas e minúsculas. Ative-o só quando você confiar nas claims de e-mail verificado dos seus IdPs a ponto de associá-las a contas que já existem.
As recusas account_exists e email_unverified usam o mesmo texto para todas as contas, então o formulário de login não pode ser usado para descobrir qual endereço tem uma função especial. account_exists também é a resposta quando o endereço corresponde a mais de uma conta, quando a busca falha e quando dois logins disputam a criação da mesma conta.
Em uma implantação com conta de cobrança
Uma implantação pode definir uma conta de cobrança que paga por todos os usuários da instalação; veja Carteiras externas. As regras dessa conta são mais rígidas, porque ela detém os créditos da implantação:
- Ela é vinculada no primeiro login verificado, seja qual for o valor de
EXTERNAL_SSO_LINK_EXISTING. Uma asserção não verificada para ela é recusada comemail_unverified, e uma asserção de outro provedor, comaccount_linked_other_provider. - Todo login posterior é verificado de novo. O e-mail precisa estar verificado, e o subject precisa ser o que vinculou a conta pela primeira vez. Caso contrário, o login é recusado com
403 account_linked_other_subject. - Ela mantém uma senha como acesso de emergência, para o dia em que o IdP não conseguir emitir asserções para ela. Em uma implantação só com SSO, esse formulário fica em
/login?billing=1e só aceita a conta de cobrança. - Ela não pode ser banida nem excluída por um administrador da instalação (
403 payer_account_protected), para que uma função de administrador concedida pelo próprio IdP da implantação não possa deixar a instalação sem acesso aos próprios créditos.
Se o subject do IdP para a conta de cobrança mudar, o login dela por SSO continua falhando com account_linked_other_subject. A conta ainda pode entrar com a senha.
Os endereços dos operadores da plataforma (PLATFORM_OPERATOR_EMAILS, ou PLATFORM_OWNER_EMAIL quando aquele está vazio) nunca recebem uma vinculação nova em uma implantação com conta de cobrança. Eles são recusados com account_exists, o que mantém as contas de operador fora do SSO. Uma conta de operador que já está vinculada continua entrando.
Provedores OIDC e SAML
Os tipos oidc e saml repassam o login ao Supabase Auth da própria instalação, que verifica a resposta do IdP. Os dois têm limites:
oidc: a página de login passa oiddo provedor como nome do provedor OAuth. Então o próprioidprecisa ser o nome de um provedor OAuth configurado no Supabase Auth da instalação, comokeycloakouazure.saml: a página de login passa oiddo provedor como domínio SAML. Umidnão pode conter ponto, então um domínio comoacme.comnão pode ser representado, e osamlnão funciona de ponta a ponta.- Uma asserção enviada a um provedor
oidcousamlé recusada com400 not_assertion_provider.
Endpoints
Os dois endpoints são públicos e não precisam de token. Eles são as únicas rotas em /v1/sso/.
| Método | Caminho | O que faz |
|---|---|---|
GET | /v1/sso/providers | Lista os provedores para a página de login, só com id, label e kind, nunca com um segredo. Retorna uma lista vazia quando o SSO está desativado. |
GET | /v1/sso/:provider | O endpoint de troca. É um endpoint de redirecionamento do navegador, não uma API JSON. |
| Status | Código | Quando |
|---|---|---|
400 | no_assertion | O botão foi clicado, e o provedor não tem initiateUrl. |
400 | not_assertion_provider | Uma asserção foi enviada a um provedor oidc ou saml. |
401 | invalid_signature, invalid_claims, expired, too_long_lived, missing_jti, missing_email, assertion_replayed | A asserção violou uma regra do contrato. |
403 | email_unverified, account_exists, account_linked_other_provider, account_linked_other_subject | As regras de vinculação de contas recusaram o login. |
403 | sso_required | Uma implantação só com SSO recusou uma sessão que não veio pelo SSO. |
404 | unknown_provider | Nenhum provedor tem esse id, ou o SSO está desativado. |
429 | rate_limit_exceeded | Requisições demais de um único endereço IP. |
Notas de segurança
- Use um segredo dedicado. O
secreté uma chave de verificação só para o Nodaro, separada do segredo de sessão do próprio IdP. - As asserções funcionam uma vez. A verificação de
jtirecusa uma asserção já enviada na segunda vez, e o limite de tempo de vida mantém a janela curta. - Os redirecionamentos ficam na instalação.
nextsó é respeitado como caminho relativo na mesma instalação, começando com/, mas não com//. Qualquer outro valor leva a/projects. - Nenhum segredo sai do servidor.
GET /v1/sso/providersretorna sóid,labelekind. A asserção e o token de uso único são removidos dos logs de requisições. - A troca tem limite de taxa por endereço IP.
Perguntas frequentes
Páginas relacionadas
Login único (SSO)
Edições e perfis de superfície
Configuração
Carteiras externas
Autenticação
Última atualização
Apps OAuth
Registre um app OAuth, envie os usuários à tela de consentimento do Nodaro, troque o código por um token de acesso de 90 dias e chame a API em nome deles.
Catálogos de seletores
Crie seletores do Nodaro em seu app com @nodaro/prompts, converta seleções em trechos de prompt, traduza rótulos, mostre imagens ou leia catálogos via API.