# 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.

Source: https://nodaro.ai/pt-BR/docs/developers/sso

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](#oidc-and-saml-providers). |

## 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.

```json
[
{
"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](#oidc-and-saml-providers). |
| `supabaseProvider` | `oidc` | Validado 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:

```ts

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](#how-accounts-are-linked).
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](https://nodaro.ai/docs/self-hosting/editions-and-profiles).

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](https://nodaro.ai/docs/developers/external-wallet). 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é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 `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.

## Frequently asked questions

### O SSO vem ativado por padrão no Nodaro?

Não. Até que EXTERNAL_SSO_PROVIDERS defina pelo menos um provedor, a página de login não mostra nenhum botão de SSO, GET /v1/sso/providers retorna uma lista vazia e todas as rotas de provedor respondem 404.

### Como a asserção de SSO precisa ser assinada?

Com HS256 e um segredo que você configura para o provedor e não usa para mais nada. A asserção também precisa trazer aud, exp, um jti único e o e-mail, e o Nodaro aceita cada asserção uma única vez.

### Uma asserção de SSO pode assumir uma conta existente com o mesmo e-mail?

Não por padrão. Uma conta local existente só é vinculada quando EXTERNAL_SSO_LINK_EXISTING é true e a asserção diz que o e-mail foi verificado. Uma conta já vinculada a outro provedor nunca é vinculada de novo.

### Um usuário de SSO é um tipo especial de conta do Nodaro?

Não. Depois da troca, o usuário tem uma sessão comum. O SSO é só uma forma de iniciar essa sessão.

### O Nodaro aceita provedores de identidade OIDC e SAML?

Em parte. Os tipos oidc e saml repassam o login ao próprio serviço de autenticação da instalação, com os limites descritos nesta página. A troca de asserção é o caminho que funciona de ponta a ponta.
