> ## 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 y JWKS: verificar los JWT de Didit Authentication

> Tokens de Didit Authentication: intercambio PKCE, claims ES256 de access e ID token, JWKS por aplicación, rotación de claves y reuso de refresh.

> [!WARNING]
>
> La autenticación es una función nueva. Todas las llamadas de esta página usan credenciales del **usuario final** o ninguna. Nunca envíes tu clave de API de verificación ni un token de la consola a estos endpoints.

Cada aplicación es su propio emisor de OpenID Connect. El emisor es el origen de inicio de sesión de tu aplicación:

* `https://<name>.auth.didit.me` para una aplicación de producción,
* `https://<name>.sandbox.auth.didit.me` para una aplicación de sandbox,
* o tu propio dominio personalizado, una vez que esté activo.

Producción y sandbox son emisores distintos con claves distintas. Asigna a `DIDIT_AUTH_ISSUER` tu emisor, sin barra final, y a `DIDIT_APPLICATION_ID` el ID de tu aplicación: los tokens lo incluyen como `aud`. Tu clave publicable codifica el emisor: después de `pk_live_` o `pk_test_`, contiene el emisor en base64url sin relleno.

> [!NOTE]
>
> Cuando tu dominio personalizado se activa, sustituye al host predeterminado como emisor. Los tokens nuevos lo indican en `iss`, el documento de descubrimiento apunta `jwks_uri` a él y tu clave publicable cambia. A partir de ese momento, los tokens de acceso emitidos con el host predeterminado no funcionan con `GET /session/` hasta que se renuevan. Actualiza `DIDIT_AUTH_ISSUER` cuando actives el dominio, o acepta ambos emisores hasta que expiren los tokens anteriores, en un máximo de 5 minutos.

Los endpoints de servidor de esta página (descubrimiento, claves, tokens y sesión) están en la [referencia de la API de autenticación](https://docs.didit.me/es/authentication/api-reference/get-openid-configuration.md), generada a partir de [openapi-authentication.json](https://docs.didit.me/openapi-authentication.json). La página de inicio de sesión alojada no forma parte de la referencia.

## Descubrimiento y claves

| Solicitud | Resultado |
| - | - |
| `GET {issuer}/.well-known/openid-configuration` | `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri` y lo que admite el emisor. |
| `GET {issuer}/.well-known/jwks.json` | `{"keys": [...]}`, las claves públicas de firma. |

El emisor admite:

* Tipo de respuesta `code` y modo de respuesta `query`.
* Tipos de concesión `authorization_code` y `refresh_token`.
* Solo el método PKCE `S256`. Los clientes son públicos: no hay secreto de cliente y `client_id` es el ID de tu aplicación.
* Algoritmo de firma `ES256` (claves P-256).
* Scopes `openid`, `email`, `phone`, `profile` y `offline_access`.

No hay endpoints de userinfo, introspección ni cierre de sesión, y no se admite `prompt=none`. Usa [`GET /session/`](#comprobar-la-sesión) para leer y comprobar una sesión.

### Caché y rotación

Ambos documentos incluyen `Cache-Control: public, max-age=300`, así que tú y las cachés compartidas pueden conservarlos durante 5 minutos. Admiten 1.200 solicitudes por minuto y 12.000 por hora por dirección IP, muy por encima de los endpoints de inicio de sesión, porque muchas instancias de backend suelen compartir una misma dirección.

* Cada clave tiene un `kid`; los nuevos ID de clave empiezan por `k_`. Cada token indica su clave en el encabezado `kid`.
* Las claves rotan cada 30 días. Didit publica la siguiente clave un día antes de que empiece a firmar y mantiene publicada la clave anterior durante un día después de que deje de hacerlo. Por eso, un JWKS en caché durante 5 minutos sigue verificando a lo largo de una rotación.
* Guarda las claves en caché por `kid`. Cuando un token indique un `kid` que no tienes, vuelve a obtener el JWKS, como máximo una vez por minuto, y rechaza el token si la clave sigue sin aparecer.
* Cuando Didit revoca una clave comprometida, la elimina del JWKS de inmediato, y `GET /session/` rechaza todos los tokens firmados con esa clave. Una clave de reemplazo empieza a firmar inmediatamente, sin el día de publicación anticipada. Un JWKS guardado en caché hasta 5 minutos antes sigue aceptando la clave revocada y todavía no conoce la de reemplazo, por lo que los tokens nuevos pueden fallar la verificación sin conexión hasta que la caché se actualice.

## Iniciar sesión

Abre la página alojada, que es el `authorization_endpoint`:

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

| Parámetro | Regla |
| - | - |
| `redirect_uri` | Debe coincidir exactamente con una de tus [URL de redirección registradas](https://docs.didit.me/es/authentication/overview.md#url-de-redirección). |
| `state` | Hasta 512 caracteres. Compruébalo en el callback. |
| `code_challenge` | El SHA-256 en base64url de tu verificador PKCE: exactamente 43 caracteres. |
| `code_challenge_method` | `S256`. |
| `client_id` | Opcional. Debe ser igual al ID de tu aplicación. |
| `nonce` | Opcional. Se copia en el ID token, para que puedas vincularlo a este inicio de sesión. |
| `scope`, `login_hint`, `ui_locales`, `max_age`, `prompt` | Opcionales. `login_hint` solo precompleta el campo de correo electrónico. |

Las [guías de inicio rápido](https://docs.didit.me/es/authentication/quickstarts/javascript.md) construyen esta solicitud por ti.

## Intercambiar el código

El callback recibe `code` y `state`. Comprueba `state` y, después, intercambia el código en un plazo de 60 segundos. Cada código solo se puede usar una vez. El cuerpo puede ser JSON o estar codificado como formulario.

```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 los segundos que le quedan al token de acceso. Si desactivas las sesiones múltiples, este intercambio cierra las demás sesiones de la persona.

| Respuesta | Significado |
| - | - |
| `400 {"error": "invalid_grant"}` | El código es desconocido, ya se usó o ha expirado; el verificador o `redirect_uri` no coinciden, o `redirect_uri` ya no está registrada; la persona está bloqueada o su correo electrónico ya no está verificado; o desactivaste el método de inicio de sesión que usó. |
| `400 {"error": "invalid_client"}` | `client_id` no es el ID de tu aplicación. |
| `400 {"error": "mfa_required"}` | El inicio de sesión no cumplió tu política de MFA. |
| `400 {"<field>": [...]}` | Falta un campo o no es válido, por ejemplo `{"non_field_errors": ["invalid_request"]}` cuando a una concesión le faltan sus campos obligatorios. |
| `400 {"<field>": "unknown_field"}` | El cuerpo contiene un campo que el endpoint no acepta, por ejemplo `client_secret`. Los clientes públicos no envían ninguno. |

## Claims del token de acceso

El token de acceso es un JWT con el encabezado `alg: ES256` y `typ: at+jwt`. Dura como máximo 5 minutos y nunca más que su sesión.

| Claim | Significado |
| - | - |
| `iss` | Tu emisor. |
| `aud` | El ID de tu aplicación. |
| `client_id` | El ID de tu aplicación. |
| `sub` | El ID de identidad de la persona en esta aplicación. |
| `sid` | El ID de la sesión. En el token de un agente de IA, el ID del acceso que la persona concedió al agente. |
| `jti`, `iat`, `exp` | ID del token, momento de emisión y expiración. |
| `vendor_data` | La referencia estable de la persona, compartida con la verificación y la monitorización de transacciones. |
| `amr` | El método de inicio de sesión, por ejemplo `["passkey"]`. |
| `org` | `{"id", "role"}` de la membresía de organización más antigua de la persona en tu aplicación, o `null` cuando no tiene ninguna o las organizaciones están desactivadas. `GET /session/` lista todas las membresías. |
| `verified` | El nivel de verificación y los claims que seleccionaste, o `{}`. Una comprobación aprobada durante una sesión se refleja en el token en la siguiente actualización. |
| `act` | `null` para una persona. Para un agente de IA que actúa en nombre de la persona: `{"sub", "agent", "scopes"}`. Consulta el [paso 5](#verificar-un-token-en-tu-servidor). |

El token de ID tiene el encabezado `typ: JWT`. Lleva los mismos claims salvo `client_id`, además de `auth_time` y `email_verified`, y `email`, `name` y `phone_number` cuando tienen valor. Solo el token de ID del intercambio del código lleva `nonce`, cuando el inicio de sesión tenía uno: compruébalo ahí, porque los tokens de ID de una actualización nunca lo llevan. Un token de ID nunca se acepta como token de acceso.

## Verificar un token en tu servidor

Comprueba todo lo siguiente antes de confiar en un claim:

1. `alg` es `ES256` y `typ` es `at+jwt`. Rechaza cualquier otro algoritmo, incluido `none`.
2. El `kid` está en el JWKS de tu aplicación y la firma se verifica.
3. `iss` es igual al emisor que configuraste y `aud` es igual al ID de tu aplicación.
4. `exp` e `iat` son válidos, con un margen de hasta 60 segundos de diferencia de reloj, y `sub`, `sid` y `jti` están presentes.
5. `act` es `null`. El token de acceso de un agente de IA se firma con la misma clave y lleva los mismos `typ`, `iss` y `aud`; solo `act` lo distingue. Recházalo, salvo que el endpoint acepte agentes: en ese caso, permite solo lo que conceda `act.scopes`. `GET /session/` siempre rechaza el token de un agente.

Toma el emisor de tu configuración, nunca del 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
```

La verificación offline no puede detectar una sesión que terminó hace un momento. Cuando eso importe, por ejemplo antes de un pago, llama a `GET /session/`.

## Comprobar la sesión

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

La respuesta contiene `sub`, `sid`, `application_id`, `vendor_data`, `amr`, `mfa_verified`, `email`, `email_verified`, `phone`, `verified_identity`, `verified`, `expires_at`, `post_logout_redirect_uri` y `organizations`. Cada entrada de organización tiene `organization_id`, `organization__name`, `role` y `custom_role`.

Esta comprobación es más estricta que la verificación offline. También falla cuando la sesión se revocó, quedó inactiva o superó su duración, cuando la persona fue bloqueada o su correo electrónico ya no está verificado, cuando la sesión ya no cumple tu política de MFA (por ejemplo, después de que la persona añadiera una app de autenticación o pasara a ser administradora de una organización), o cuando el token se renovó. Un fallo devuelve `401 {"detail": "invalid_token"}` con `WWW-Authenticate: Bearer`.

`GET /session/` admite 30 solicitudes por minuto y 300 por hora por dirección IP, un límite que comparte con `DELETE /session/`. Verifica los tokens offline en cada solicitud y reserva esta llamada para los momentos que la necesiten. Cada llamada exitosa cuenta como actividad, así que reinicia el `idle_timeout` de la sesión.

Un correo electrónico verificado no es una verificación de identidad. Para eso, lee `verified`: aquí es `null` cuando no hay ninguna, mientras que los tokens llevan `{}`.

## Tokens de actualización

```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"}'
```

Una actualización devuelve un nuevo token de acceso, un nuevo token de ID y un nuevo token de actualización, y el token de actualización que enviaste deja de funcionar.

* El token de actualización es una cadena aleatoria, válida hasta que termina la sesión.
* **Reutilizarlo termina la sesión.** Cuando vuelve a llegar un token de actualización ya usado, Didit lo trata como una señal de robo: revoca la sesión y devuelve `400 {"error": "invalid_grant"}`. Los tokens de la sesión también dejan de funcionar, incluidos los de la actualización anterior: el token de actualización deja de funcionar y el token de acceso deja de funcionar de inmediato al invocar `GET /session/`, aunque sigue verificándose offline hasta que expira, en un plazo de 5 minutos.
* Ejecuta una sola actualización a la vez por sesión, en todas las pestañas, dispositivos e instancias de servidor, y sustituye los tres tokens a la vez.
* Si una actualización falla con `400` o no obtienes respuesta, vuelve a iniciar sesión. No la reintentes a ciegas: reintentar una actualización que sí se completó es una reutilización.
* Un `503 {"error": "temporarily_unavailable"}` significa que la actualización no se realizó y que tu token de actualización sigue funcionando. Vuelve a intentarlo con backoff.
* Didit responde con un `429` antes de ejecutar la actualización, así que reintenta después de `Retry-After`. El endpoint de tokens admite 30 solicitudes por minuto y 300 por hora por dirección IP. Además, todas las solicitudes `POST` y `DELETE` de tu aplicación comparten los contadores de protección contra bots: 60 solicitudes por minuto por dirección IP entre esos endpoints, 30 por minuto por dispositivo y 1,000 por minuto para toda la aplicación, incluidos los navegadores y los pasos de inicio de sesión.
* Con tokens de acceso de 5 minutos, 300 actualizaciones por hora cubren a unas 25 personas que se mantienen activas. Si tu servidor actualiza tokens para muchas personas desde una misma dirección, actualiza solo cuando una solicitud necesite un token nuevo y, cuando puedas, hazlo en el navegador o en la app.

## Cerrar sesión

`DELETE /session/` con el token de acceso actual de la persona finaliza la sesión y devuelve `204`. Su token de actualización deja de funcionar y sus tokens de acceso dejan de ser válidos para `GET /session/` de inmediato; los tokens que verificas sin conexión siguen siendo válidos hasta que expiran. Después, envía a la persona a tu `post_logout_redirect_uri`, si configuraste uno.

Para cerrar sesión se necesita un token de acceso que `GET /session/` aceptaría: uno que no haya expirado ni haya sido reemplazado tras renovar el token. Un `401` significa que la sesión no finalizó. Renueva el token y luego cierra sesión con el nuevo token de acceso; si la renovación falla con `400`, la sesión ya no se puede usar.

Una sesión también finaliza cuando:

* supera `session_lifetime` (30 días por defecto) o `idle_timeout` (1 día por defecto),
* la persona vuelve a iniciar sesión cuando no se permiten varias sesiones,
* la persona restablece su contraseña mientras `revoke_on_password_change` está activado,
* tu equipo la revoca o bloquea a la persona en la consola,
* se vuelve a enviar un token de actualización ya usado.

## Duración

| Elemento | Duración |
| - | - |
| Transacción de inicio de sesión (`POST /authorize/`) | 10 minutos |
| Código de autorización | 60 segundos, un solo uso |
| Token de acceso | 5 minutos como máximo |
| Token de ID | El mismo `exp` que el token de acceso emitido con él |
| Token de actualización | Hasta que finaliza la sesión, un solo uso |
| Sesión | `session_lifetime` y `idle_timeout` |
| Verificación reforzada (step-up) | 30 minutos |
| Caché de descubrimiento y JWKS | 5 minutos |

## Guarda las credenciales según su propósito

* Guarda las claves de API de verificación y los tokens de la consola en tu servidor.
* Guarda los tokens de acceso del navegador en memoria siempre que sea posible. En aplicaciones renderizadas en el servidor, usa cookies seguras HttpOnly y protege contra CSRF las solicitudes autenticadas con cookies.
* Guarda los tokens de actualización en aplicaciones móviles en el almacenamiento protegido por la plataforma. Borra el estado local después de cerrar sesión.
* Nunca pongas tokens en las URL ni en los logs.

Continúa con la [guía de inicio rápido de JavaScript](https://docs.didit.me/es/authentication/quickstarts/javascript.md), la [guía de inicio rápido de Next.js](https://docs.didit.me/es/authentication/quickstarts/nextjs.md) o la [referencia de la API de autenticación](https://docs.didit.me/es/authentication/api-reference/get-openid-configuration.md).


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