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

# Tokens e JWKS: verifique JWTs da Didit Authentication

> Didit Authentication: troca PKCE, claims ES256 de access e ID token, discovery e JWKS por aplicação, rotação de chaves e detecção de reuso.

> [!WARNING]
>
> A autenticação é nova. Todas as chamadas desta página usam credenciais de **usuário final** ou nenhuma credencial. Nunca envie sua chave de API de verificação nem um token do console para esses endpoints.

Cada aplicação é um emissor OpenID Connect próprio. O emissor é a origem de login da sua aplicação:

* `https://<name>.auth.didit.me` para uma aplicação em produção,
* `https://<name>.sandbox.auth.didit.me` para uma aplicação de sandbox,
* ou seu próprio domínio personalizado, depois de ativado.

Produção e sandbox são emissores separados, com chaves separadas. Defina `DIDIT_AUTH_ISSUER` como o seu emissor, sem barra no final, e `DIDIT_APPLICATION_ID` como o ID da sua aplicação: os tokens o carregam como `aud`. Sua chave publicável codifica o emissor: depois de `pk_live_` ou `pk_test_`, vem o emissor em base64url sem preenchimento.

> [!NOTE]
>
> Quando o seu domínio personalizado fica ativo, ele substitui o host padrão como emissor. Os novos tokens o indicam em `iss`, o documento de descoberta aponta `jwks_uri` para ele e sua chave publicável muda. Os tokens de acesso emitidos pelo host padrão passam então a falhar em `GET /session/` até serem renovados. Atualize `DIDIT_AUTH_ISSUER` ao ativar o domínio ou aceite os dois emissores até que os tokens antigos expirem, em até 5 minutos.

Os endpoints de servidor desta página (descoberta, chaves, tokens e sessão) estão na [referência da API de autenticação](https://docs.didit.me/pt-br/authentication/api-reference/get-openid-configuration.md), gerada a partir de [openapi-authentication.json](https://docs.didit.me/openapi-authentication.json). A página de login hospedada não está incluída nela.

## Descoberta e chaves

| Requisição | Resultado |
| - | - |
| `GET {issuer}/.well-known/openid-configuration` | `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri` e o que o emissor suporta. |
| `GET {issuer}/.well-known/jwks.json` | `{"keys": [...]}`, as chaves públicas de assinatura. |

O emissor suporta:

* Tipo de resposta `code` e modo de resposta `query`.
* Tipos de concessão `authorization_code` e `refresh_token`.
* Apenas o método PKCE `S256`. Os clientes são públicos: não há segredo de cliente, e `client_id` é o ID da sua aplicação.
* Algoritmo de assinatura `ES256` (chaves P-256).
* Escopos `openid`, `email`, `phone`, `profile` e `offline_access`.

Não há endpoint de userinfo, de introspecção nem de encerramento de sessão, e `prompt=none` não é suportado. Use [`GET /session/`](#verificar-a-sessão) para ler e verificar uma sessão.

### Cache e rotação

Os dois documentos respondem com `Cache-Control: public, max-age=300`, então você e caches compartilhados podem mantê-los por 5 minutos. Eles permitem 1.200 requisições por minuto e 12.000 por hora por endereço IP, bem acima dos endpoints de login, porque muitas instâncias de backend costumam compartilhar um mesmo endereço.

* Cada chave tem um `kid`; os novos IDs de chave começam com `k_`. Todo token indica sua chave no cabeçalho `kid`.
* As chaves são rotacionadas a cada 30 dias. A Didit publica a próxima chave um dia antes de ela começar a assinar e mantém a chave anterior publicada por um dia depois que ela deixa de assinar. Assim, um JWKS em cache por 5 minutos continua verificando durante uma rotação.
* Armazene as chaves em cache por `kid`. Quando um token indicar um `kid` que você não tem, busque o JWKS novamente, no máximo uma vez por minuto, e rejeite o token se a chave continuar ausente.
* Quando a Didit revoga uma chave comprometida, ela remove a chave do JWKS imediatamente, e `GET /session/` rejeita todos os tokens assinados por essa chave. Uma chave substituta começa a assinar na hora, sem o dia de publicação antecipada. Um JWKS em cache há até 5 minutos ainda aceita a chave revogada e ainda não conhece a substituta, então os novos tokens podem falhar na verificação offline até o cache ser atualizado.

## Iniciar o login

Abra a página hospedada, que é o `authorization_endpoint`:

```text theme={null}
{issuer}/sign-in?redirect_uri=...&state=...&code_challenge=...&code_challenge_method=S256
```

| Parâmetro | Regra |
| - | - |
| `redirect_uri` | Deve corresponder exatamente a uma das suas [URLs de redirecionamento registradas](https://docs.didit.me/pt-br/authentication/overview.md#urls-de-redirecionamento). |
| `state` | Até 512 caracteres. Verifique-o na URL de retorno. |
| `code_challenge` | O SHA-256 do seu verificador PKCE codificado em base64url: exatamente 43 caracteres. |
| `code_challenge_method` | `S256`. |
| `client_id` | Opcional. Deve ser igual ao ID da sua aplicação. |
| `nonce` | Opcional. É copiado para o ID token, para que você possa vinculá-lo a este login. |
| `scope`, `login_hint`, `ui_locales`, `max_age`, `prompt` | Opcionais. `login_hint` apenas preenche previamente o campo de e-mail. |

Os [guias de início rápido](https://docs.didit.me/pt-br/authentication/quickstarts/javascript.md) montam essa requisição para você.

## Trocar o código

O callback recebe `code` e `state`. Verifique `state` e então troque o código em até 60 segundos. Cada código funciona uma única vez. O corpo pode ser JSON ou codificado como formulário.

```bash theme={null}
curl --fail-with-body "${DIDIT_AUTH_ISSUER}/token/" \
  -H 'Content-Type: application/json' \
  -d '{
    "grant_type": "authorization_code",
    "code": "CODE_FROM_CALLBACK",
    "code_verifier": "YOUR_PKCE_VERIFIER",
    "redirect_uri": "https://app.example.com/auth/callback"
  }'
```

```json theme={null}
{
  "access_token": "eyJhbGciOiJFUzI1NiIs...",
  "id_token": "eyJhbGciOiJFUzI1NiIs...",
  "refresh_token": "N4yZTl6y24YKYq77X_SO...",
  "token_type": "Bearer",
  "expires_in": 299,
  "session_id": "c9abf6cc-2d64-4bf2-b31e-a4d09a070ab1"
}
```

`expires_in` indica os segundos que restam ao token de acesso. Quando você desativa as sessões múltiplas, essa troca encerra as outras sessões da pessoa.

| Resposta | Significado |
| - | - |
| `400 {"error": "invalid_grant"}` | O código é desconhecido, já foi usado ou expirou; o verificador ou o `redirect_uri` não corresponde, ou o `redirect_uri` não está mais registrado; a pessoa está bloqueada ou o e-mail dela não está mais verificado; ou você desativou o método de login que ela usou. |
| `400 {"error": "invalid_client"}` | `client_id` não é o ID da sua aplicação. |
| `400 {"error": "mfa_required"}` | O login não atendeu à sua política de MFA. |
| `400 {"<field>": [...]}` | Um campo está ausente ou é inválido, por exemplo `{"non_field_errors": ["invalid_request"]}` quando uma concessão não tem os campos obrigatórios. |
| `400 {"<field>": "unknown_field"}` | O corpo tem um campo que o endpoint não aceita, por exemplo `client_secret`. Clientes públicos não enviam nenhum. |

## Claims do token de acesso

O token de acesso é um JWT com cabeçalho `alg: ES256` e `typ: at+jwt`. Ele é válido por no máximo 5 minutos e nunca por mais tempo do que sua sessão.

| Claim | Significado |
| - | - |
| `iss` | Seu emissor. |
| `aud` | O ID da sua aplicação. |
| `client_id` | O ID da sua aplicação. |
| `sub` | O ID de identidade da pessoa nesta aplicação. |
| `sid` | O ID da sessão. No token de um agente de IA, é o ID do acesso que a pessoa concedeu ao agente. |
| `jti`, `iat`, `exp` | ID do token, momento de emissão e expiração. |
| `vendor_data` | A referência estável da pessoa, compartilhada com a verificação e o monitoramento de transações. |
| `amr` | O método de login, por exemplo `["passkey"]`. |
| `org` | `{"id", "role"}` da associação mais antiga da pessoa a uma organização na sua aplicação, ou `null` quando ela não tem nenhuma ou quando as organizações estão desativadas. `GET /session/` lista todas as associações. |
| `verified` | O nível de verificação e as claims que você selecionou, ou `{}`. Uma verificação aprovada durante uma sessão é incluída no token na próxima renovação. |
| `act` | `null` para uma pessoa. Para um agente de IA que age em nome da pessoa: `{"sub", "agent", "scopes"}`. Veja a [etapa 5](#verificar-um-token-no-seu-servidor). |

O ID token tem cabeçalho `typ: JWT`. Ele traz as mesmas claims, exceto `client_id`, e inclui também `auth_time` e `email_verified`, além de `email`, `name` e `phone_number` quando têm valor. Somente o ID token da troca do código traz `nonce`, quando o login incluía um: verifique-o ali, porque os ID tokens de uma renovação nunca o trazem. Um ID token nunca é aceito como token de acesso.

## Verificar um token no seu servidor

Verifique todos estes pontos antes de confiar em uma claim:

1. `alg` é `ES256` e `typ` é `at+jwt`. Rejeite qualquer outro algoritmo, inclusive `none`.
2. O `kid` está no JWKS da sua aplicação, e a assinatura é válida.
3. `iss` é igual ao emissor configurado, e `aud` é igual ao ID da sua aplicação.
4. `exp` e `iat` são válidos, com tolerância de até 60 segundos de diferença de relógio, e `sub`, `sid` e `jti` estão presentes.
5. `act` é `null`. O token de acesso de um agente de IA é assinado com a mesma chave e traz os mesmos `typ`, `iss` e `aud`; só `act` o diferencia. Rejeite-o, a menos que o endpoint aceite agentes: nesse caso, permita apenas o que `act.scopes` concede. `GET /session/` sempre rejeita o token de um agente.

Obtenha o emissor da sua configuração, nunca do token.

```javascript Node.js theme={null}
// npm install jose
import { createRemoteJWKSet, jwtVerify } from "jose";

const ISSUER = process.env.DIDIT_AUTH_ISSUER; // https://<name>.auth.didit.me
const AUDIENCE = process.env.DIDIT_APPLICATION_ID;
// Cache keys for 5 minutes; refetch an unknown kid at most once a minute.
const jwks = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`), {
  cacheMaxAge: 5 * 60 * 1000,
  cooldownDuration: 60 * 1000,
});

export async function verifyAccessToken(token) {
  const { payload } = await jwtVerify(token, jwks, {
    issuer: ISSUER,
    audience: AUDIENCE,
    algorithms: ["ES256"],
    typ: "at+jwt",
    clockTolerance: 60,
    maxTokenAge: "5m", // also rejects an iat in the future
    requiredClaims: ["sub", "sid", "jti", "exp", "iat"],
  });
  if (payload.act !== null) throw new Error("agent tokens are not accepted");
  return payload; // payload.vendor_data is the person across Didit products
}
```

```python Python theme={null}
# pip install "pyjwt[crypto]"
import os
import threading
import time

import jwt

ISSUER = os.environ["DIDIT_AUTH_ISSUER"]  # https://<name>.auth.didit.me
AUDIENCE = os.environ["DIDIT_APPLICATION_ID"]
jwks = jwt.PyJWKClient(f"{ISSUER}/.well-known/jwks.json", lifespan=300)
refresh_lock = threading.Lock()
last_refresh = 0.0


def signing_key(kid: str | None) -> jwt.PyJWK:
    # PyJWKClient refetches for every unknown kid: limit it to once a minute.
    global last_refresh
    keys = {key.key_id: key for key in jwks.get_signing_keys()}
    if kid not in keys:
        with refresh_lock:
            if time.monotonic() - last_refresh >= 60:
                last_refresh = time.monotonic()
                fresh = jwks.get_signing_keys(refresh=True)
                keys = {key.key_id: key for key in fresh}
    if kid not in keys:
        raise jwt.InvalidTokenError("unknown signing key")
    return keys[kid]


def verify_access_token(token: str) -> dict:
    header = jwt.get_unverified_header(token)
    if header.get("typ") != "at+jwt":
        raise jwt.InvalidTokenError("not an access token")
    claims = jwt.decode(
        token,
        signing_key(header.get("kid")).key,
        algorithms=["ES256"],
        audience=AUDIENCE,
        issuer=ISSUER,
        leeway=60,
        options={"require": ["iss", "aud", "sub", "sid", "jti", "exp", "iat"]},
    )
    if "act" not in claims or claims["act"] is not None:
        raise jwt.InvalidTokenError("agent tokens are not accepted")
    return claims
```

A verificação offline não consegue detectar uma sessão que acabou de ser encerrada. Quando isso importa, por exemplo antes de um pagamento, chame `GET /session/`.

## Verificar a sessão

```bash theme={null}
curl --fail-with-body "${DIDIT_AUTH_ISSUER}/session/" \
  -H "Authorization: Bearer $DIDIT_ACCESS_TOKEN"
```

A resposta contém `sub`, `sid`, `application_id`, `vendor_data`, `amr`, `mfa_verified`, `email`, `email_verified`, `phone`, `verified_identity`, `verified`, `expires_at`, `post_logout_redirect_uri` e `organizations`. Cada entrada de organização tem `organization_id`, `organization__name`, `role` e `custom_role`.

Esta verificação é mais rigorosa do que a verificação offline. Ela também falha quando a sessão foi revogada, ficou inativa ou ultrapassou seu tempo de vida, quando a pessoa foi bloqueada ou seu e-mail deixou de estar verificado, quando a sessão não atende mais à sua política de MFA (por exemplo, depois que a pessoa adicionou um app autenticador ou se tornou administradora de uma organização) ou quando o token foi substituído por uma renovação. Uma falha retorna `401 {"detail": "invalid_token"}` com `WWW-Authenticate: Bearer`.

`GET /session/` permite 30 requisições por minuto e 300 por hora por endereço IP, um limite compartilhado com `DELETE /session/`. Verifique os tokens offline em toda requisição e reserve esta chamada para os momentos que exigem isso. Cada chamada bem-sucedida conta como atividade, então ela reinicia o `idle_timeout` da sessão.

Um e-mail verificado não é uma verificação de identidade. Para isso, leia `verified`: aqui ele é `null` quando não há verificação, enquanto os tokens trazem `{}`.

## Tokens de renovação

```bash theme={null}
curl --fail-with-body "${DIDIT_AUTH_ISSUER}/token/" \
  -H 'Content-Type: application/json' \
  -d '{"grant_type": "refresh_token", "refresh_token": "YOUR_REFRESH_TOKEN"}'
```

Uma renovação retorna um novo token de acesso, um novo ID token e um novo token de renovação, e o token de renovação que você enviou deixa de funcionar.

* O token de renovação é uma string aleatória, válida até o fim da sessão.
* **A reutilização encerra a sessão.** Quando um token de renovação já usado chega novamente, a Didit trata isso como sinal de roubo: ela revoga a sessão e retorna `400 {"error": "invalid_grant"}`. Os tokens da sessão também deixam de funcionar, inclusive os da renovação anterior: o token de renovação deles falha, e as requisições `GET /session/` com esses tokens de acesso falham imediatamente, embora ainda seja validado offline até expirar, em até 5 minutos.
* Execute uma renovação por vez para cada sessão, considerando abas, dispositivos e instâncias de servidor, e substitua os três tokens juntos.
* Se uma renovação falhar com `400` ou não houver resposta, inicie o login novamente. Não tente novamente às cegas: uma nova tentativa após uma renovação que de fato deu certo é uma reutilização.
* Um `503 {"error": "temporarily_unavailable"}` significa que a renovação não aconteceu e que seu token de renovação continua funcionando. Tente novamente com backoff.
* A Didit responde com `429` antes de executar a renovação, então tente novamente depois de `Retry-After`. O endpoint de token permite 30 requisições por minuto e 300 por hora por endereço IP. Além disso, todos os `POST` e `DELETE` da sua aplicação compartilham buckets de proteção contra bots: 60 requisições por minuto por endereço IP somando esses endpoints, 30 por minuto por dispositivo e 1.000 por minuto para a aplicação inteira, incluindo navegadores e etapas de login.
* Com tokens de acesso de 5 minutos, 300 renovações por hora atendem cerca de 25 pessoas que permanecem ativas. Se o seu servidor faz renovações para muitas pessoas a partir de um único endereço, renove apenas quando uma requisição precisar de um novo token e, sempre que possível, faça a renovação no navegador ou no app.

## Sair da sessão

`DELETE /session/` com o access token atual da pessoa encerra a sessão e retorna `204`. O refresh token dela deixa de funcionar, e os access tokens dela passam a falhar em `GET /session/` imediatamente; tokens que você verifica offline continuam válidos até expirarem. Em seguida, redirecione a pessoa para o seu `post_logout_redirect_uri`, se você tiver definido um.

Sair da sessão exige um access token que `GET /session/` aceitaria: um que não tenha expirado e não tenha sido substituído por um refresh. Um `401` significa que a sessão não foi encerrada. Faça o refresh e depois saia da sessão com o novo access token; se o refresh falhar com `400`, a sessão não pode mais ser usada.

Uma sessão também é encerrada quando:

* ela ultrapassa `session_lifetime` (padrão de 30 dias) ou `idle_timeout` (padrão de 1 dia),
* a pessoa entra novamente enquanto múltiplas sessões estão desativadas,
* a pessoa redefine a senha enquanto `revoke_on_password_change` está ativado,
* sua equipe a revoga ou bloqueia a pessoa no console,
* um refresh token já usado é enviado novamente.

## Tempos de vida

| Item | Tempo de vida |
| - | - |
| Transação de login (`POST /authorize/`) | 10 minutos |
| Código de autorização | 60 segundos, uso único |
| Access token | 5 minutos, no máximo |
| ID token | O mesmo `exp` do access token emitido com ele |
| Refresh token | Até a sessão ser encerrada, uso único |
| Sessão | `session_lifetime` e `idle_timeout` |
| Step-up | 30 minutos |
| Cache de discovery e JWKS | 5 minutos |

## Armazene credenciais de acordo com a finalidade

* Mantenha as chaves de API de verificação e os tokens do console no seu servidor.
* Mantenha os access tokens do navegador em memória sempre que possível. Para aplicações renderizadas no servidor, use cookies HttpOnly seguros e proteja as requisições autenticadas por cookie contra CSRF.
* Armazene os refresh tokens de dispositivos móveis no armazenamento protegido da plataforma. Limpe o estado local depois de sair da sessão.
* Nunca coloque tokens em URLs ou logs.

Continue com o [início rápido em JavaScript](https://docs.didit.me/pt-br/authentication/quickstarts/javascript.md), o [início rápido em Next.js](https://docs.didit.me/pt-br/authentication/quickstarts/nextjs.md) ou a [referência da API de autenticação](https://docs.didit.me/pt-br/authentication/api-reference/get-openid-configuration.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.