> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Identidade federada

> Você já tem autenticação. Assine o id do usuário no seu servidor e o UserKit confia.

No modo federado o seu produto é dono da conta. O UserKit precisa saber *qual* dos
seus usuários está na página, e não vai aceitar a palavra da página — o seu
**servidor** assina a afirmação.

A prova é um HMAC:

```
hash = hex( HMAC-SHA256( identity_secret, external_id ) )
```

<Warning>
  O `identity_secret` nunca chega ao navegador. Calcule o hash no seu servidor,
  renderize na página e entregue ao boot. Um segredo no bundle é um segredo na mão de
  cada visitante — e este emite sessões *verificadas*.
</Warning>

## Configuração

<Steps>
  <Step title="Coloque o ambiente em federado">
    ```bash theme={null}
    curl -s -X PATCH $API/v1/organization/environments/{id} \
      -H "Authorization: Bearer $UK_SESSION" \
      -H "X-Organization-Id: org_4b1e…" \
      -H 'Content-Type: application/json' \
      -d '{ "auth_mode": "federated" }'
    ```

    Exige `organization:update`.
  </Step>

  <Step title="Leia o segredo de identidade">
    ```bash theme={null}
    curl -s $API/v1/organization/environments/{id}/identity \
      -H "Authorization: Bearer $UK_SESSION" \
      -H "X-Organization-Id: org_4b1e…"
    ```

    ```json theme={null}
    {
      "environment_id": "…",
      "auth_mode": "federated",
      "identity_secret": "uk_is_7c2a…",
      "rotated_at": null
    }
    ```

    Exige `api_keys:write` — este segredo emite sessões verificadas, então lê-lo é
    possuí-lo, e uma trava de leitura seria a trava errada. Ele é criado na primeira
    leitura: até você tê-lo lido, não existe HMAC que ele pudesse verificar.
  </Step>

  <Step title="Coloque no ambiente do seu servidor">
    ```bash theme={null}
    USERKIT_IDENTITY_SECRET=uk_is_7c2a…
    ```

    Por ambiente. O seu deploy de teste guarda o segredo do ambiente de teste.
  </Step>
</Steps>

## Assine a afirmação

<CodeGroup>
  ```ts Node theme={null}
  import { createHmac } from "node:crypto";

  export function userkitHash(externalId: string) {
    return createHmac("sha256", process.env.USERKIT_IDENTITY_SECRET!)
      .update(externalId)
      .digest("hex");
  }
  ```

  ```go Go theme={null}
  func userkitHash(externalID string) string {
  	mac := hmac.New(sha256.New, []byte(os.Getenv("USERKIT_IDENTITY_SECRET")))
  	mac.Write([]byte(externalID))
  	return hex.EncodeToString(mac.Sum(nil))
  }
  ```

  ```python Python theme={null}
  import hmac, hashlib, os

  def userkit_hash(external_id: str) -> str:
      return hmac.new(
          os.environ["USERKIT_IDENTITY_SECRET"].encode(),
          external_id.encode(),
          hashlib.sha256,
      ).hexdigest()
  ```

  ```ruby Ruby theme={null}
  require "openssl"

  def userkit_hash(external_id)
    OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("USERKIT_IDENTITY_SECRET"), external_id)
  end
  ```
</CodeGroup>

A mensagem é o `external_id` e nada mais. Sem timestamp, sem e-mail, sem
concatenação.

## Boot

```bash theme={null}
curl -s $API/v1/boot \
  -H 'Origin: https://app.example.com' \
  -H 'Content-Type: application/json' \
  -d '{
    "publishable_key": "uk_pk_live_…",
    "external_id": "user_8421",
    "hash": "9a3f…",
    "email": "grace@example.com",
    "anonymous_id": "anon_2f9c1b"
  }'
```

```json 200 (ou 201 quando o contato foi criado) theme={null}
{
  "contact": { "id": "3f9a…", "identified": true },
  "token": "uk_ct_…",
  "expires_at": "2026-08-28T12:00:00Z",
  "environment": "live",
  "verified": true
}
```

### Os três desfechos do hash

| `hash`              | Resultado                                                                                            |
| ------------------- | ---------------------------------------------------------------------------------------------------- |
| Válido              | Sessão **verificada**                                                                                |
| Ausente             | Sessão **não verificada** — usável em desenvolvimento, marcada no painel, e barrada das rotas abaixo |
| Presente mas errado | `401 invalid_identity_hash`                                                                          |

Um hash errado é recusado de saída e nunca rebaixado a não verificado. Aceitar em
silêncio uma assinatura ruim tornaria invisível um erro de digitação no seu código
de HMAC — você subiria e descobriria muito depois.

### O que uma sessão sem prova não faz

Um boot sem hash afirma um `external_id` e não comprova nada — e é uma chamada
que qualquer um que saiba o `external_id` de alguém consegue fazer do navegador.
Então a sessão que ele emite para nas rotas onde essa afirmação passaria a
importar, cada uma com `403 unverified_session`:

| Rota                                | Por quê                                                                                                                                               |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /v1/contact/token`            | O JWT é o que o seu próprio backend trata como prova de identidade — viraria uma requisição autenticada lá dentro a partir de uma afirmação sem prova |
| `GET`/`DELETE /v1/contact/sessions` | Lista e revoga os aparelhos e endereços de onde a pessoa entrou                                                                                       |
| `POST /v1/contact/two-factor/*`     | Administra o segundo fator de alguém, inclusive desligando                                                                                            |

`GET /v1/contact/me` não está na lista: o `boot` já respondeu com o contato que
resolveu, então barrar isso não fecharia nada.

**Sessões anônimas não são afetadas.** A sessão de visitante também é não
verificada, mas não há quem personificar — a linha foi criada pela mesma chamada
de `boot` que agora a segura, então os dados dela são do próprio chamador por
construção. A regra é sobre a única combinação em que a conta do outro lado é de
outra pessoa: não verificada *e* identificada.

O caminho para passar é o que você ia querer em produção de todo jeito:
configurar o segredo de identidade e mandar o hash.

### Um boot sem prova não reescreve perfil

Um boot sem hash pode **criar** um contato, e é sempre um avistamento. O que ele
não pode é mudar o `name` ou o `email` de um contato que já existe.

Esses dois são para onde vão os avisos de segurança e de onde sai a claim
`email` do JWT. Se uma afirmação sem prova pudesse sobrescrevê-los, quem a fez
passaria a receber os avisos da vítima. Um contato que a própria chamada acabou
de criar não tem dono anterior de quem tirar nada, então fica com o que chegou.

### O e-mail é atributo, sempre

Esta é a regra que mantém o modo federado seguro, e ela é estrutural em vez de uma
verificação.

Um e-mail que chega por `/v1/boot` é guardado na linha do contato. Ele **não**
resolve para um contato existente, e **não** vira aresta de identidade.

O HMAC comprova o `external_id` e só ele. Se um e-mail na mesma requisição pudesse
resolver para um contato, então um hash válido para o *seu próprio* id somado ao
e-mail de *outra pessoa* alcançaria os dados dessa pessoa. É a forma clássica de
tomada de conta, e está fechada por construção.

Para comprovar um e-mail no modo federado, envie um
[magic link](/pt-br/customer-auth/hosted#magic-links) — o único fluxo que promove um
e-mail de atributo a identidade comprovada, e funciona nos dois modos.

## Boot anônimo

Boot sem `external_id` é um avistamento de visitante, e funciona nos **dois** modos:

```bash theme={null}
curl -s $API/v1/boot \
  -H 'Origin: https://example.com' \
  -H 'Content-Type: application/json' \
  -d '{
    "publishable_key": "uk_pk_live_…",
    "anonymous_id": "anon_2f9c1b",
    "attribution": {
      "utm_source": "newsletter",
      "utm_campaign": "july-launch",
      "referrer": "https://news.ycombinator.com/",
      "landing_page": "https://example.com/pricing"
    }
  }'
```

É aqui que a **atribuição de primeiro toque nasce** — no contato anônimo, antes de
essa pessoa ter um nome. Quando ela depois entrar e o boot carregar `anonymous_id` e
`external_id` juntos, o visitante é absorvido pelo contato identificado e a
atribuição vem junto.

Envie as UTMs da página, onde elas de fato estão.

## Rotacionando o segredo

```bash theme={null}
curl -s -X POST $API/v1/organization/environments/{id}/identity/rotate \
  -H "Authorization: Bearer $UK_SESSION" \
  -H "X-Organization-Id: org_4b1e…"
```

```json theme={null}
{ "environment_id": "…", "identity_secret": "uk_is_new…", "grace_hours": 24 }
```

O segredo anterior continua verificando por **24 horas** — tempo suficiente para uma
frota redeployar, curto o bastante para que um segredo antigo roubado tenha prazo.
Suba o novo valor dentro da janela; depois dela, hashes assinados com o segredo
antigo são recusados.

## Modos de falha

| Resposta                             | Causa                                                                                                 |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| `401 invalid_identity_hash`          | Segredo errado, segredo do ambiente errado, ou a mensagem assinada não era exatamente o `external_id` |
| `409 environment_not_federated`      | O ambiente está em modo hosted                                                                        |
| `403 origin_not_allowed`             | A origem da página não está na lista da chave publicável                                              |
| `429 rate_limited`                   | 240 boots por minuto por IP, ou por chave publicável                                                  |
| `501 federated_identity_unavailable` | A identidade federada está indisponível no servidor                                                   |

<Note>
  `GET /v1/organization/environments/{id}/identity` é relegível de propósito: o seu
  servidor precisa do valor numa variável de ambiente, e um segredo que só pode ser
  visto uma vez é um segredo que alguém cola num chat.
</Note>
