Docs do Nodaro
DocumentaçãoReferência de nósModelosAgentes de IA (MCP)DesenvolvedoresSelf-hostingPesquisa

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

TipoParaComo funciona
assertionEmissores que não falam OIDC nem SAML, como um host de incorporação que só consegue gerar um token assinado de curta duraçãoO 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, samlIdPs que falam OpenID Connect ou SAML padrãoA 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"
  }
]
CampoAplica-se aSignificado
idTodosO 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.
labelTodosO nome de exibição do provedor.
kindTodosassertion, oidc ou saml.
secretassertion, obrigatórioA 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.
audienceassertion, obrigatórioO valor a que a claim aud da asserção precisa ser igual.
claimMapassertion, opcionalQuais claims trazem o e-mail, o indicador de verificação e o subject. O padrão é email, email_verified e sub.
initiateUrlassertion, opcionalPara onde o botão de SSO envia o usuário. Sem ele, um clique no botão responde 400 no_assertion.
initiateUrlByHostassertion, opcionalUm 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.
maxLifetimeSecondsassertion, opcionalO maior tempo de vida (exp menos iat) que o Nodaro aceita. O padrão é 300 segundos, e o máximo é 3.600.
domainoidc, samlValidado na inicialização, mas não usado. Veja Provedores OIDC e SAML.
supabaseProvideroidcValidado na inicialização, mas não usado.
  • initiateUrl nã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 initiateUrlByHost sã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 HS256 e com o secret do provedor.
  • Audiência. aud é igual ao audience do provedor.
  • Expiração. exp está presente e não está no passado, com uma tolerância de 5 segundos para diferenças de relógio.
  • Tempo de vida. exp menos iat é no máximo maxLifetimeSeconds, medido no relógio do Nodaro. Um iat mais de 5 segundos no futuro é limitado, e um iat ausente conta como agora.
  • Uso único. jti está presente e é único. O Nodaro lembra cada jti até 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 é true para 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

  1. O usuário clica no botão de SSO na página de login do Nodaro, que chama GET /v1/sso/:provider sem asserção.
  2. O Nodaro redireciona (302) para o initiateUrl do provedor.
  3. O IdP autentica o usuário e envia o navegador de volta para GET /v1/sso/:provider?assertion=<jwt>, opcionalmente com &next=<path>.
  4. O Nodaro verifica a asserção, recusa reutilizações e aplica as regras de vinculação de contas.
  5. O Nodaro redireciona para /sso?sso_token=<one-time token>. Essa página troca o token de uso único por uma sessão.
  6. O usuário chega a /projects ou ao caminho de next.

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 sso em auth.methods e define auth.ssoLabel. Um perfil sem ssoLabel remove sso dos métodos. O texto do botão é o ssoLabel.
  • 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çãoResultado
Nenhuma conta tem o e-mail, e o e-mail está verificadoUma conta nova é criada e vinculada ao provedor.
Nenhuma conta tem o e-mail, e o e-mail não está verificadoRecusado 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 provedorO usuário entra.
A conta está vinculada a outro provedorRecusado 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á verificadoA 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á verificadoRecusado 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 com email_unverified, e uma asserção de outro provedor, com account_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=1 e 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 o id do provedor como nome do provedor OAuth. Então o próprio id precisa ser o nome de um provedor OAuth configurado no Supabase Auth da instalação, como keycloak ou azure.
  • saml: a página de login passa o id do provedor como domínio SAML. Um id não pode conter ponto, então um domínio como acme.com não pode ser representado, e o saml não funciona de ponta a ponta.
  • Uma asserção enviada a um provedor oidc ou saml é recusada com 400 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étodoCaminhoO que faz
GET/v1/sso/providersLista 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/:providerO endpoint de troca. É um endpoint de redirecionamento do navegador, não uma API JSON.
StatusCódigoQuando
400no_assertionO botão foi clicado, e o provedor não tem initiateUrl.
400not_assertion_providerUma asserção foi enviada a um provedor oidc ou saml.
401invalid_signature, invalid_claims, expired, too_long_lived, missing_jti, missing_email, assertion_replayedA asserção violou uma regra do contrato.
403email_unverified, account_exists, account_linked_other_provider, account_linked_other_subjectAs regras de vinculação de contas recusaram o login.
403sso_requiredUma implantação só com SSO recusou uma sessão que não veio pelo SSO.
404unknown_providerNenhum provedor tem esse id, ou o SSO está desativado.
429rate_limit_exceededRequisiçõ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 jti recusa 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. next só é 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/providers retorna só id, label e kind. 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

Última atualização

Nesta página