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

# Guia técnico do cliente

> Implementação OAuth 2.1 + PKCE contra a Notifique: cliente remoto, local, DCR, refresh rotativo e revogação.

<Tip>
  RFC na prática — remoto, loopback, PKCE e refresh rotativo. Caminho rápido: [Quick Start](/oauth-api/como-funciona/quick-start).
</Tip>

Este guia é para quem implementa o fluxo **do zero** ou com biblioteca OAuth. Visão geral: [Construir um cliente](/oauth-api/como-funciona/construir-cliente).

A Notifique implementa **OAuth 2.1** com **PKCE obrigatório** em todo authorization code exchange, mais **Dynamic Client Registration (DCR)** em `POST /oauth/register`. Issuer: `https://api.notifique.dev`.

A tela de login e consentimento fica no **painel Notifique**. Seu app só abre a URL de autorização no browser e trata o callback — você **não** constrói UI de consentimento.

## Tipos de cliente

| Tipo             | Secret | Quando usar                | Auth em `/oauth/token`                    |
| ---------------- | :----: | -------------------------- | ----------------------------------------- |
| **Público**      |   Não  | SPA, mobile, CLI, MCP host | PKCE + `token_endpoint_auth_method: none` |
| **Confidencial** |   Sim  | Web app com backend        | PKCE + `client_secret_basic`              |

PKCE é **sempre** obrigatório — mesmo em clientes confidenciais.

## Caminhos recomendados

Antes de codar, decida:

1. **Registro fixo vs DCR**
   * **Pré-registrado** (painel ou `POST /oauth/register` uma vez) — apps web e integrações conhecidas.
   * **DCR em runtime** — MCP hosts e CLIs que não sabem redirect/porta antes de rodar.

2. **Remoto vs local**
   * **Remoto** — redirect `https://seuapp.com/oauth/callback`, backend guarda `state` e `code_verifier` na sessão.
   * **Local** — loopback `http://127.0.0.1:<porta>/callback`, servidor temporário na máquina do usuário.

## Escopos

Peça o **mínimo** no registro **e** no authorize. Escopos omitidos no DCR podem ser interpretados de forma ampla — declare explicitamente.

Exemplos Notifique:

* Só e-mail: `email:send`
* WhatsApp: `whatsapp:send`
* Vários: `email:send contacts:read` (separados por espaço, URL-encoded na query)

Lista completa: [Escopos](/oauth-api/como-funciona/escopos).

## Encoding e endpoints

| Endpoint               | Content-Type preferido              |
| ---------------------- | ----------------------------------- |
| `POST /oauth/register` | `application/json`                  |
| `POST /oauth/token`    | `application/x-www-form-urlencoded` |
| `POST /oauth/revoke`   | `application/x-www-form-urlencoded` |

Cliente confidencial autentica token/revoke com **HTTP Basic** (`client_id:client_secret`), não repita `client_id` no body se usar `-u`.

Metadados: `GET /.well-known/oauth-authorization-server` (RFC 8414).

## Gerar PKCE e state

Antes do authorize, gere três valores:

* **`code_verifier`** — string aleatória de alta entropia; fica só no cliente até o token.
* **`code_challenge`** — `BASE64URL(SHA256(code_verifier))`.
* **`state`** — aleatório; deve voltar **igual** no callback (proteção CSRF).

```javascript theme={null}
import { createHash, randomBytes } from 'node:crypto';

function base64url(input) {
  return Buffer.from(input).toString('base64url');
}

const codeVerifier = base64url(randomBytes(64));
const codeChallenge = base64url(
  createHash('sha256').update(codeVerifier).digest(),
);
const state = base64url(randomBytes(24));
```

**Remoto:** persista `state` e `codeVerifier` na sessão do usuário antes do redirect.

**Local:** mantenha em memória enquanto o servidor loopback estiver ativo.

***

## Cliente remoto pré-registrado

Redirect **HTTPS** fixo, ex.: `https://example.com/oauth/callback`.

Registre como confidencial (`client_secret_basic`). O `client_secret` aparece **uma vez** — guarde em cofre de secrets, nunca no frontend.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Backend do app
    participant B as Browser
    participant AS as api.notifique.dev
    participant D as Painel Notifique

    Note over C: Gera PKCE + state<br>Salva na sessão

    C->>B: 302 para /oauth/authorize
    B->>AS: GET authorize (client_id, scope, state, code_challenge)
    AS-->>B: 302 para tela de consentimento
    B->>D: Usuário aprova escopos
    D-->>B: 302 redirect_uri?code&state
    B->>C: Callback code + state

    Note over C: Valida state

    C->>AS: POST /oauth/token (Basic + code_verifier)
    AS-->>C: access_token + refresh_token
```

### Checklist do callback

Trate como **falha** se:

* Faltar `code`
* `state` não bater com o salvo
* Vier `error` na query (`access_denied`, etc.)

### URL de autorização (exemplo)

```
GET /oauth/authorize?client_id=ntf_oauth_cl_abc&response_type=code&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback&scope=email%3Asend&state=STATE&code_challenge=CHALLENGE&code_challenge_method=S256
Host: api.notifique.dev
```

### Trocar o code

```bash theme={null}
curl -X POST 'https://api.notifique.dev/oauth/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -u 'CLIENT_ID:CLIENT_SECRET' \
  -d 'grant_type=authorization_code&code=AUTH_CODE&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback&code_verifier=CODE_VERIFIER'
```

Resposta:

```json theme={null}
{
  "access_token": "eyJhbGciOiJFZERTQSIs...",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "rt_...",
  "scope": "email:send"
}
```

***

## Cliente local (loopback)

Redirect em **`127.0.0.1`** com porta efêmera, ex.: `http://127.0.0.1:49152/oauth/callback`.

Registre como **público** (`token_endpoint_auth_method: none`). Faça bind em `127.0.0.1` ou `[::1]` — **não** use `0.0.0.0`.

<Warning>
  Não faça prefetch de `/oauth/authorize` no backend e siga redirects server-side. O **usuário** precisa ver a tela de consentimento no browser.
</Warning>

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as CLI / app local
    participant B as Browser
    participant AS as api.notifique.dev
    participant D as Painel Notifique

    Note over C: Sobe servidor loopback<br>Gera PKCE + state

    C->>AS: POST /oauth/register (redirect loopback)
    AS-->>C: client_id

    C->>B: Abre URL /oauth/authorize
    B->>D: Usuário aprova
    D-->>B: redirect loopback?code&state
    B->>C: Callback

    Note over C: Valida state, fecha servidor

    C->>AS: POST /oauth/token (code_verifier, sem secret)
    AS-->>C: access_token + refresh_token
```

### DCR para CLI

```bash theme={null}
curl -X POST 'https://api.notifique.dev/oauth/register' \
  -H 'Content-Type: application/json' \
  -d '{
    "client_name": "Minha CLI",
    "redirect_uris": ["http://127.0.0.1/oauth/callback"],
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "token_endpoint_auth_method": "none",
    "scope": "email:send"
  }'
```

Na autorização, use a porta real (`http://127.0.0.1:49152/oauth/callback`) se o path base foi registrado com flexibilidade de porta — host e path devem bater com o cadastro.

### Abrir authorize (Node)

```javascript theme={null}
const url = new URL('https://api.notifique.dev/oauth/authorize');
url.search = new URLSearchParams({
  client_id: 'ntf_oauth_cl_abc',
  response_type: 'code',
  redirect_uri: 'http://127.0.0.1:49152/oauth/callback',
  scope: 'email:send',
  state,
  code_challenge: codeChallenge,
  code_challenge_method: 'S256',
}).toString();

openBrowser(url.toString());
```

Callback esperado:

```
GET /oauth/callback?code=AUTH_CODE&state=STATE
Host: 127.0.0.1:49152
```

### Token (público)

```bash theme={null}
curl -X POST 'https://api.notifique.dev/oauth/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code&client_id=CLIENT_ID&code=AUTH_CODE&redirect_uri=http%3A%2F%2F127.0.0.1%3A49152%2Foauth%2Fcallback&code_verifier=CODE_VERIFIER'
```

Feche o servidor loopback após sucesso ou timeout.

***

## Refresh token rotativo

Cada refresh bem-sucedido devolve um **novo** `refresh_token`. O anterior invalida.

<Warning>
  **Serialize** refresh por grant (um worker por vez). Salve o novo refresh **atomicamente** com o resto da resposta. Se dois workers refresharem com o mesmo token antigo, o grant inteiro pode ser revogado.
</Warning>

```bash theme={null}
curl -X POST 'https://api.notifique.dev/oauth/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -u 'CLIENT_ID:CLIENT_SECRET' \
  -d 'grant_type=refresh_token&refresh_token=rt_...'
```

Cliente público: inclua `client_id` no body em vez de Basic auth.

***

## Chamar `/v1` com access token

Access token é **JWT** (\~15 min, EdDSA). Envie como Bearer:

```bash theme={null}
curl -X POST 'https://api.notifique.dev/v1/email/messages' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{ "from": "Suporte <noreply@dominio.com>", "to": ["a@b.com"], "subject": "Teste", "text": "..." }'
```

Validação offline opcional: chaves em `GET /.well-known/jwks.json`.

***

## Revogar acesso

Revogue o **refresh token** — access tokens JWT não são revogados individualmente; revogar o refresh invalida o grant.

```bash theme={null}
curl -X POST 'https://api.notifique.dev/oauth/revoke' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -u 'CLIENT_ID:CLIENT_SECRET' \
  -d 'token=rt_...&token_type_hint=refresh_token'
```

No workspace: **Configurações → Equipe → Apps conectados**.

***

## Erros comuns (implementação)

| Erro             | O que verificar                                                                           |
| ---------------- | ----------------------------------------------------------------------------------------- |
| `invalid_grant`  | `code` expirado, `redirect_uri` diferente entre authorize e token, `code_verifier` errado |
| `invalid_client` | Secret errado ou cliente público enviando Basic auth indevido                             |
| `invalid_scope`  | Escopo inexistente ou não permitido para o cliente                                        |
| Refresh morto    | Token já rotacionado — reauthorize ou use o último refresh salvo                          |
| Consent loop     | Prefetch server-side do authorize sem browser do usuário                                  |

## Próximos passos

* [Quick Start](/oauth-api/como-funciona/quick-start)
* [MCP](/oauth-api/como-funciona/mcp)
* [OpenAPI — OAuth](/oauth-api/api-reference/openapi-oauth.json)
