> ## 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: verificar JWT de autenticação Didit

> Tokens de autenticação Didit: troca PKCE, claims ES256 dos tokens de acesso e ID, JWKS por aplicação, rotação de chaves e deteção de reutilização.

> [!WARNING]
>
> A autenticação é nova. Todas as chamadas desta página usam credenciais de **utilizador final** ou nenhuma. Nunca envie a sua chave de API de verificação nem um token da consola para estes endpoints.

Cada aplicação é o seu próprio emissor OpenID Connect. O emissor é a origem de início de sessão 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 sandbox,
* ou o seu domínio personalizado, depois de estar ativo.

Os emissores de produção e sandbox são separados e têm chaves separadas. Defina `DIDIT_AUTH_ISSUER` com o seu emissor, sem barra final, e `DIDIT_APPLICATION_ID` com o ID da sua aplicação: os tokens incluem-no como `aud`. A sua chave publicável codifica o emissor: depois de `pk_live_` ou `pk_test_`, está o emissor em base64url sem padding.

> [!NOTE]
>
> Quando o seu domínio personalizado ficar ativo, substitui o host predefinido como emissor. Os novos tokens indicam-no em `iss`, a descoberta aponta `jwks_uri` para ele, e a sua chave publicável muda. Os tokens de acesso emitidos com o host predefinido falham em `GET /session/` até serem atualizados. Atualize `DIDIT_AUTH_ISSUER` quando ativar o domínio, ou aceite os dois emissores até os tokens antigos expirarem, em até 5 minutos.

Os endpoints do 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/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 início de sessão alojada não está incluída.

## Descoberta e chaves

| Pedido | 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`.
* Método PKCE `S256` apenas. Os clientes são públicos: não existe segredo do cliente, e `client_id` é o ID da sua aplicação.
* Algoritmo de assinatura `ES256` (chaves P-256).
* Âmbitos `openid`, `email`, `phone`, `profile` e `offline_access`.

Não existem endpoints de userinfo, introspeção nem fim de sessão, e `prompt=none` não é suportado. Use [`GET /session/`](#verifique-a-sessão) para ler e verificar uma sessão.

### Cache e rotação

Ambos os documentos respondem com `Cache-Control: public, max-age=300`, pelo que pode mantê-los em cache durante 5 minutos, tal como as caches partilhadas. Permitem 1200 pedidos por minuto e 12 000 por hora por endereço IP, limites muito superiores aos dos endpoints de início de sessão, porque muitas instâncias de backend costumam partilhar o mesmo endereço.

* Cada chave tem um `kid`; os novos IDs de chave começam por `k_`. Cada token indica a sua chave no cabeçalho `kid`.
* As chaves rodam a cada 30 dias. A Didit publica a chave seguinte um dia antes de começar a assinar, e mantém a chave anterior publicada durante um dia depois de deixar de assinar. Um JWKS mantido em cache durante 5 minutos permite, por isso, continuar a verificar assinaturas durante uma rotação.
* Coloque as chaves em cache por `kid`. Quando um token indicar um `kid` que não tem, vá buscar de novo o JWKS, no máximo uma vez por minuto, e rejeite o token se a chave continuar em falta.
* Quando a Didit revoga uma chave comprometida, remove-a imediatamente do JWKS, e `GET /session/` rejeita todos os tokens assinados por essa chave. Uma chave de substituição começa a assinar de imediato, sem o dia de publicação antecipada. Um JWKS em cache até 5 minutos antes continua a aceitar a chave revogada e ainda não conhece a substituta, pelo que os novos tokens podem falhar a verificação offline até a cache ser atualizada.

## Iniciar sessão

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

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

| Parâmetro | Regra |
| - | - |
| `redirect_uri` | Tem de corresponder exatamente a um dos seus [URLs de redirecionamento registados](https://docs.didit.me/pt/authentication/overview.md#urls-de-redirecionamento). |
| `state` | Até 512 caracteres. Verifique-o no callback. |
| `code_challenge` | O SHA-256 em base64url do seu verificador PKCE: exatamente 43 caracteres. |
| `code_challenge_method` | `S256`. |
| `client_id` | Opcional. Tem de ser igual ao ID da sua aplicação. |
| `nonce` | Opcional. É copiado para o token de ID, para que o possa associar a este início de sessão. |
| `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/authentication/quickstarts/javascript.md) criam este pedido por si.

## Trocar o código

O callback recebe `code` e `state`. Verifique `state` e, depois, troque o código em até 60 segundos. Cada código funciona uma única vez. O corpo pode ser JSON ou codificado no formato de 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 o token de acesso ainda tem de validade. Quando desativa várias sessões, esta troca termina 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 correspondem, ou o `redirect_uri` já não está registado; a pessoa está bloqueada ou o seu e-mail já não está verificado; ou desativou o método de início de sessão que a pessoa usou. |
| `400 {"error": "invalid_client"}` | `client_id` não é o ID da sua aplicação. |
| `400 {"error": "mfa_required"}` | O início de sessão não cumpre a sua política de MFA. |
| `400 {"<field>": [...]}` | Um campo está em falta 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`. Os clientes públicos não enviam nenhum. |

## Afirmações do token de acesso

O token de acesso é um JWT com o cabeçalho `alg: ES256` e `typ: at+jwt`. Tem uma duração máxima de 5 minutos e nunca dura mais do que a sua sessão.

| Afirmação | Significado |
| - | - |
| `iss` | O 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, data de emissão e data de expiração. |
| `vendor_data` | A referência estável da pessoa, partilhada com a verificação e a monitorização de transações. |
| `amr` | O método de início de sessão, por exemplo `["passkey"]`. |
| `org` | `{"id", "role"}` da filiação mais antiga da pessoa numa organização da sua aplicação, ou `null` quando não tem nenhuma ou quando as organizações estão desativadas. `GET /session/` lista todas as filiações. |
| `verified` | O nível de verificação e as afirmações que selecionou, ou `{}`. Uma verificação aprovada durante uma sessão chega ao token na atualização seguinte. |
| `act` | `null` para uma pessoa. Para um agente de IA que atua em nome da pessoa: `{"sub", "agent", "scopes"}`. Veja o [passo 5](#verifique-um-token-no-seu-servidor). |

O token de ID tem o cabeçalho `typ: JWT`. Contém as mesmas afirmações, exceto `client_id`, e ainda `auth_time` e `email_verified`, bem como `email`, `name` e `phone_number` quando têm valor. Apenas o token de ID da troca do código contém `nonce`, quando o início de sessão o tinha: verifique-o aí, porque os tokens de ID das atualizações nunca o contêm. Um token de ID nunca é aceite como token de acesso.

## Verifique um token no seu servidor

Verifique todos estes pontos antes de confiar numa afirmação:

1. `alg` é `ES256` e `typ` é `at+jwt`. Rejeite qualquer outro algoritmo, incluindo `none`.
2. O `kid` está no JWKS da sua aplicação, e a assinatura é validada.
3. `iss` é igual ao emissor configurado, e `aud` é igual ao ID da sua aplicação.
4. `exp` e `iat` são válidos, admitindo 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 tem o mesmo `typ`, `iss` e `aud`; só `act` o distingue. Rejeite-o, a não ser que o endpoint aceite agentes: nesse caso, permita apenas o que `act.scopes` concede. `GET /session/` rejeita sempre o token de um agente.

Obtenha o emissor a partir da sua configuração, nunca a partir 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 ver uma sessão que terminou há instantes. Quando isso importa, por exemplo antes de um pagamento, chame `GET /session/`.

## Verifique 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. Falha também quando a sessão foi revogada, ficou inativa ou ultrapassou a sua duração máxima, quando a pessoa foi bloqueada ou o seu e-mail deixou de estar verificado, quando a sessão deixa de cumprir a sua política de MFA (por exemplo, depois de a pessoa adicionar uma aplicação de autenticação ou de se tornar administradora de uma organização), ou quando o token foi substituído por uma atualização. Uma falha devolve `401 {"detail": "invalid_token"}` com `WWW-Authenticate: Bearer`.

`GET /session/` permite 30 pedidos por minuto e 300 por hora por endereço IP, limite que partilha com `DELETE /session/`. Verifique os tokens offline em cada pedido e reserve esta chamada para os momentos que a exigem. Cada chamada bem-sucedida conta como atividade e, por isso, reinicia o `idle_timeout` da sessão.

Um e-mail verificado não é uma verificação de identidade. Para isso, leia `verified`: aqui é `null` quando não existe, enquanto os tokens contêm `{}`.

## Tokens de atualizaçã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 atualização devolve um novo token de acesso, um novo token de ID e um novo token de atualização, e o token de atualização que enviou deixa de funcionar.

* O token de atualização é uma cadeia aleatória, válida até a sessão terminar.
* **A reutilização termina a sessão.** Quando um token de atualização já usado volta a chegar, a Didit interpreta-o como sinal de roubo: revoga a sessão e devolve `400 {"error": "invalid_grant"}`. Os tokens da sessão também deixam de funcionar, incluindo os da atualização anterior: o respetivo token de atualização falha e o seu token de acesso falha de imediato em `GET /session/`, embora continue a ser validado offline até expirar, no máximo em 5 minutos.
* Execute uma só atualização de cada vez para uma sessão, em todos os separadores, dispositivos e instâncias do servidor, e substitua os três tokens em conjunto.
* Se uma atualização falhar com `400` ou não obtiver resposta, volte a iniciar sessão. Não tente de novo às cegas: uma nova tentativa depois de uma atualização bem-sucedida constitui uma reutilização.
* Um `503 {"error": "temporarily_unavailable"}` significa que a atualização não aconteceu e que o seu token de atualização continua a funcionar. Tente novamente com espera progressiva.
* A Didit responde com um `429` antes de a atualização ser executada, por isso tente novamente após `Retry-After`. O endpoint de tokens permite 30 pedidos por minuto e 300 por hora por endereço IP. Além disso, cada `POST` e `DELETE` da sua aplicação partilha baldes de proteção contra bots: 60 pedidos por minuto por endereço IP nesses endpoints e 30 por minuto por dispositivo. Para a aplicação inteira, as atualizações têm um balde de 6.000 por minuto, e cada outro `POST` e `DELETE` (incluindo os de navegadores e passos de início de sessão) tem um de 3.000 por minuto, pelo que as atualizações nunca consomem os seus pedidos de início de sessão.
* Com tokens de acesso de 5 minutos, 300 atualizações por hora cobrem cerca de 25 pessoas que permanecem ativas. Se o seu servidor atualizar os tokens de muitas pessoas a partir do mesmo endereço, atualize apenas quando um pedido precisar de um novo token, e faça a atualização no navegador ou na aplicação sempre que for possível.

## Terminar sessão

`DELETE /session/` com o token de acesso atual da pessoa termina a sessão e devolve `204`. O respetivo token de atualização deixa de funcionar, e os seus tokens de acesso falham de imediato em `GET /session/`; os tokens que verifica offline continuam válidos até expirarem. Depois, envie a pessoa para o seu `post_logout_redirect_uri`, se tiver definido um.

Terminar sessão exige um token de acesso que `GET /session/` aceitaria: um que não tenha expirado e não tenha sido substituído por uma renovação. Um `401` significa que a sessão não terminou. Renove o token e, depois, termine a sessão com o novo token de acesso; se a renovação falhar com `400`, a sessão já não pode ser utilizada.

Uma sessão termina também quando:

* ultrapassa `session_lifetime` (predefinido: 30 dias) ou `idle_timeout` (predefinido: 1 dia),
* a pessoa inicia sessão novamente quando as sessões múltiplas estão desativadas,
* a pessoa redefine a palavra-passe com `revoke_on_password_change` ativo,
* a sua equipa revoga a sessão ou bloqueia a pessoa na consola,
* um token de atualização já utilizado é enviado novamente.

## Durações

| Item | Duração |
| - | - |
| Transação de início de sessão (`POST /authorize/`) | 10 minutos |
| Código de autorização | 60 segundos, utilização única |
| Token de acesso | 5 minutos, no máximo |
| ID token | Mesmo `exp` do token de acesso emitido com ele |
| Token de atualização | Até a sessão terminar, utilização única |
| Sessão | `session_lifetime` e `idle_timeout` |
| Autenticação reforçada | 30 minutos |
| Cache de descoberta e JWKS | 5 minutos |

## Guardar credenciais consoante a finalidade

* Guarde as chaves de API de verificação e os tokens da consola no seu servidor.
* Guarde os tokens de acesso do navegador em memória sempre que possível. Em aplicações renderizadas no servidor, use cookies seguros HttpOnly e proteja os pedidos autenticados por cookies contra CSRF.
* Guarde os tokens de atualização de dispositivos móveis em armazenamento protegido pela plataforma. Limpe o estado local após terminar a sessão.
* Nunca coloque tokens em URLs ou registos.

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


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