> ## 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.

# Verificació de JWT de Didit Authentication: tokens i JWKS

> Verifica els JWT de Didit Authentication: PKCE, claims ES256, descobriment i JWKS per aplicació, rotació de claus i reutilització de refresh tokens.

> [!WARNING]
>
> L'autenticació és nova. Totes les crides d'aquesta pàgina fan servir credencials d'usuari final o cap credencial. No enviïs mai la teva clau d'API de verificació ni un token de la consola a aquests endpoints.

Cada aplicació és el seu propi emissor OpenID Connect. L'emissor és l'origen d'inici de sessió de la teva aplicació:

* `https://<name>.auth.didit.me` per a una aplicació en producció,
* `https://<name>.sandbox.auth.didit.me` per a una aplicació de sandbox,
* o el teu domini personalitzat, quan estigui actiu.

Producció i sandbox són emissors diferents amb claus diferents. Defineix `DIDIT_AUTH_ISSUER` amb el teu emissor, sense barra final, i `DIDIT_APPLICATION_ID` amb l'ID de la teva aplicació: els tokens el porten com a `aud`. La teva clau publicable codifica l'emissor: després de `pk_live_` o `pk_test_`, és l'emissor en base64url sense farciment.

> [!NOTE]
>
> Quan el teu domini personalitzat estigui actiu, substituirà el host per defecte com a emissor. Els tokens nous l'indicaran a `iss`, el document de descoberta apuntarà `jwks_uri` cap a ell i la teva clau publicable canviarà. Els tokens d'accés emesos amb el host per defecte fallaran a `GET /session/` fins que es renovin. Actualitza `DIDIT_AUTH_ISSUER` quan activis el domini, o accepta els dos emissors fins que caducin els tokens antics, en un termini de 5 minuts.

Els endpoints de servidor d'aquesta pàgina (descoberta, claus, tokens i la sessió) són a la [Referència de l'API d'autenticació](https://docs.didit.me/ca/authentication/api-reference/get-openid-configuration.md), generada a partir de [openapi-authentication.json](https://docs.didit.me/openapi-authentication.json). La pàgina d'inici de sessió allotjada no ho és.

## Descoberta i claus

| Sol·licitud | Resultat |
| - | - |
| `GET {issuer}/.well-known/openid-configuration` | `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri` i el que l'emissor admet. |
| `GET {issuer}/.well-known/jwks.json` | `{"keys": [...]}`, les claus públiques de signatura. |

L'emissor admet:

* Tipus de resposta `code` i mode de resposta `query`.
* Tipus de concessió `authorization_code` i `refresh_token`.
* Mètode PKCE només `S256`. Els clients són públics: no hi ha secret de client, i `client_id` és l'ID de la teva aplicació.
* Algorisme de signatura `ES256` (claus P-256).
* Scopes `openid`, `email`, `phone`, `profile` i `offline_access`.

No hi ha endpoints de userinfo, d'introspecció ni de tancament de sessió, i `prompt=none` no és compatible. Fes servir [`GET /session/`](#comprova-la-sessió) per llegir i comprovar una sessió.

### Memòria cau i rotació

Tots dos documents responen amb `Cache-Control: public, max-age=300`, de manera que tu i les memòries cau compartides els podeu guardar durant 5 minuts. Permeten 1.200 sol·licituds per minut i 12.000 per hora per adreça, molt per sobre dels límits dels endpoints d'inici de sessió, perquè moltes instàncies de backend sovint comparteixen una mateixa adreça.

* Cada clau té un `kid`; els ID de clau nous comencen per `k_`. Cada token indica la seva clau a la capçalera `kid`.
* Les claus roten cada 30 dies. Didit publica la clau següent un dia abans que comenci a signar, i manté la clau anterior publicada durant un dia després que deixi de signar. Un JWKS guardat a la memòria cau durant 5 minuts, per tant, continua verificant durant una rotació.
* Guarda les claus a la memòria cau per `kid`. Quan un token indiqui un `kid` que no tens, torna a obtenir el JWKS, com a màxim una vegada per minut, i rebutja el token si la clau continua sense aparèixer.
* Quan Didit revoca una clau compromesa, l'elimina del JWKS immediatament, i `GET /session/` rebutja tots els tokens que la clau va signar. Una clau de substitució comença a signar de seguida, sense el dia d'antelació de publicació. Un JWKS guardat a la memòria cau fins a 5 minuts abans encara accepta la clau revocada i encara no coneix la substitució, de manera que els tokens nous poden fallar la verificació sense connexió fins que la memòria cau es refresqui.

## Inicia la sessió

Obre la pàgina allotjada, que és el `authorization_endpoint`:

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

| Paràmetre | Regla |
| - | - |
| `redirect_uri` | Ha de coincidir exactament amb una de les teves [URL de redirecció registrades](https://docs.didit.me/ca/authentication/overview.md#url-de-redirecció). |
| `state` | Fins a 512 caràcters. Comprova'l al callback. |
| `code_challenge` | El SHA-256 en base64url del teu verificador PKCE: exactament 43 caràcters. |
| `code_challenge_method` | `S256`. |
| `client_id` | Opcional. Ha de ser igual al teu ID d'aplicació. |
| `nonce` | Opcional. Es copia al token d'ID, de manera que pots vincular-lo a aquest inici de sessió. |
| `scope`, `login_hint`, `ui_locales`, `max_age`, `prompt` | Opcionals. `login_hint` només preemplena el camp de correu electrònic. |

Les [guies ràpides](https://docs.didit.me/ca/authentication/quickstarts/javascript.md) construeixen aquesta sol·licitud per a tu.

## Intercanvia el codi

El callback rep `code` i `state`. Comprova `state` i, després, intercanvia el codi en un termini de 60 segons. Cada codi funciona una sola vegada. El cos pot ser JSON o codificat en format de formulari.

```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 els segons que li queden al token d'accés. Quan desactives les múltiples sessions, aquest intercanvi tanca les altres sessions de la persona.

| Resposta | Significat |
| - | - |
| `400 {"error": "invalid_grant"}` | El codi és desconegut, ja s'ha usat o ha caducat; el verificador o el `redirect_uri` no coincideixen, o el `redirect_uri` ja no està registrat; la persona està bloquejada o el seu correu electrònic ja no està verificat; o has desactivat el mètode d'inici de sessió que va fer servir. |
| `400 {"error": "invalid_client"}` | `client_id` no és el teu ID d'aplicació. |
| `400 {"error": "mfa_required"}` | L'inici de sessió no compleix la teva política de MFA. |
| `400 {"<field>": [...]}` | Falta un camp o no és vàlid, per exemple `{"non_field_errors": ["invalid_request"]}` quan una concessió no té els camps obligatoris. |
| `400 {"<field>": "unknown_field"}` | El cos té un camp que l'endpoint no accepta, per exemple `client_secret`. Els clients públics no n'envien cap. |

## Claims del token d'accés

El token d'accés és un JWT amb la capçalera `alg: ES256` i `typ: at+jwt`. Dura com a màxim 5 minuts, i mai més que la seva sessió.

| Reclamació | Significat |
| - | - |
| `iss` | El teu emissor. |
| `aud` | L'ID de la teva aplicació. |
| `client_id` | L'ID de la teva aplicació. |
| `sub` | L'ID d'identitat de la persona en aquesta aplicació. |
| `sid` | L'ID de la sessió. Per al token d'un agent d'IA, l'ID de l'accés que la persona ha concedit a l'agent. |
| `jti`, `iat`, `exp` | ID del token, data d'emissió i data de caducitat. |
| `vendor_data` | La referència estable de la persona, compartida amb la verificació i el monitoratge de transaccions. |
| `amr` | El mètode d'inici de sessió, per exemple `["passkey"]`. |
| `org` | `{"id", "role"}` de la primera pertinença a una organització de la persona dins de la teva aplicació, o `null` quan no en té cap o les organitzacions estan desactivades. `GET /session/` enumera totes les pertinences. |
| `verified` | El nivell de verificació i les reclamacions que has seleccionat, o `{}`. Una comprovació aprovada durant una sessió arriba al token en la següent renovació. |
| `act` | `null` per a una persona. Per a un agent d'IA que actua en nom de la persona: `{"sub", "agent", "scopes"}`. Consulta el [pas 5](#verifica-un-token-al-teu-servidor). |

El token d'ID té la capçalera `typ: JWT`. Porta les mateixes reclamacions excepte `client_id`, a més de `auth_time` i `email_verified`, i `email`, `name` i `phone_number` quan tenen un valor. Només el token d'ID de l'intercanvi del codi porta `nonce`, quan l'inici de sessió en tenia un: comprova-ho aquí, perquè els tokens d'ID de les renovacions mai no el porten. Un token d'ID mai s'accepta com a token d'accés.

## Verifica un token al teu servidor

Comprova tot això abans de confiar en una reclamació:

1. `alg` és `ES256` i `typ` és `at+jwt`. Rebutja qualsevol altre algorisme, inclòs `none`.
2. El `kid` és al JWKS de la teva aplicació, i la signatura es verifica.
3. `iss` és igual al teu emissor configurat, i `aud` és igual a l'ID de la teva aplicació.
4. `exp` i `iat` són vàlids, amb una tolerància de fins a 60 segons de diferència de rellotge, i `sub`, `sid` i `jti` hi són.
5. `act` és `null`. El token d'accés d'un agent d'IA es signa amb la mateixa clau i porta el mateix `typ`, `iss` i `aud`; només `act` el distingeix. Rebutja'l, tret que l'endpoint accepti agents: en aquest cas, permet només el que concedeix `act.scopes`. `GET /session/` rebutja sempre el token d'un agent.

Pren l'emissor de la configuració, mai 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ó fora de línia no pot veure una sessió que ha acabat fa un moment. Quan això importa, per exemple abans d'un pagament, crida `GET /session/`.

## Comprova la sessió

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

La resposta conté `sub`, `sid`, `application_id`, `vendor_data`, `amr`, `mfa_verified`, `email`, `email_verified`, `phone`, `verified_identity`, `verified`, `expires_at`, `post_logout_redirect_uri` i `organizations`. Cada entrada d'organització té `organization_id`, `organization__name`, `role` i `custom_role`.

Aquesta comprovació és més estricta que la verificació fora de línia. També falla quan la sessió s'ha revocat, ha quedat inactiva o ha superat la seva durada, quan la persona ha estat bloquejada o el seu correu electrònic ja no està verificat, quan la sessió ja no compleix la teva política de MFA (per exemple, després que la persona hagi afegit una aplicació d'autenticació o s'hagi convertit en administradora d'una organització), o quan el token s'ha substituït en una renovació. Un error retorna `401 {"detail": "invalid_token"}` amb `WWW-Authenticate: Bearer`.

`GET /session/` permet 30 sol·licituds per minut i 300 per hora per adreça IP, un límit que comparteix amb `DELETE /session/`. Verifica els tokens fora de línia en cada sol·licitud, i reserva aquesta crida per als moments que ho necessiten. Cada crida satisfactòria compta com a activitat, de manera que reinicia el `idle_timeout` de la sessió.

Un correu electrònic verificat no és una verificació d'identitat. Llegeix `verified` per saber-ho: aquí és `null` quan no n'hi ha cap, mentre que els tokens porten `{}`.

## Tokens de renovació

```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 renovació retorna un nou token d'accés, un nou token d'ID i un nou token de renovació, i el token de renovació que has enviat deixa de funcionar.

* El token de renovació és una cadena aleatòria, vàlida fins que acaba la sessió.
* **La reutilització acaba la sessió.** Quan arriba de nou un token de renovació ja utilitzat, Didit ho considera un senyal de robatori: revoca la sessió i retorna `400 {"error": "invalid_grant"}`. Els tokens de la sessió també deixen de funcionar, inclosos els de la renovació anterior: el seu token de renovació falla i el seu token d'accés fa fallar `GET /session/` de seguida, tot i que encara es verifica fora de línia fins que caduca, en un termini de 5 minuts.
* Fes una sola renovació alhora per a una sessió, a través de pestanyes, dispositius i instàncies del servidor, i substitueix els tres tokens junts.
* Si una renovació falla amb `400` o no obté resposta, torna a iniciar la sessió. No ho reintentis a cegues: un reintent després d'una renovació que sí que ha funcionat és una reutilització.
* Un `503 {"error": "temporarily_unavailable"}` vol dir que la renovació no s'ha produït i que el teu token de renovació encara funciona. Reintenta-la amb una espera progressiva.
* Didit respon amb un `429` abans d'executar la renovació, així que reintenta-la després de `Retry-After`. L'endpoint de tokens permet 30 sol·licituds per minut i 300 per hora per adreça IP. A més, cada `POST` i `DELETE` de la teva aplicació comparteix els límits de protecció contra bots: 60 sol·licituds per minut per adreça IP entre aquests endpoints i 30 per minut per dispositiu. Per a tota l'aplicació, les renovacions tenen un límit de 6.000 per minut i cada altre `POST` i `DELETE` (inclosos els navegadors i els passos d'inici de sessió) en té un de 3.000 per minut, de manera que les renovacions mai no esgoten els teus inicis de sessió.
* Amb tokens d'accés de 5 minuts, 300 renovacions per hora cobreixen unes 25 persones que es mantenen actives. Si el teu servidor renova els tokens de molts usuaris des d'una mateixa adreça, renova només quan una sol·licitud necessiti un token nou, i fes-ho al navegador o a l'aplicació on sigui possible.

## Tancar la sessió

`DELETE /session/` amb el token d'accés actual de la persona finalitza la sessió i retorna `204`. El seu token d'actualització deixa de funcionar, i els seus tokens d'accés fallen en les sol·licituds a `GET /session/` de seguida; els tokens que verifiquis fora de línia continuen vàlids fins que caduquin. Després, envia la persona a la teva `post_logout_redirect_uri`, si n'has definit una.

Per tancar la sessió cal un token d'accés que `GET /session/` acceptaria: un que no hagi caducat i que no s'hagi substituït per un token d'actualització. Un `401` vol dir que la sessió no s'ha tancat. Renova el token i després tanca la sessió amb el nou token d'accés; si la renovació falla amb `400`, la sessió ja no es pot utilitzar.

Una sessió també finalitza quan:

* supera `session_lifetime` (per defecte, 30 dies) o `idle_timeout` (per defecte, 1 dia),
* la persona torna a iniciar sessió mentre les sessions múltiples estan desactivades,
* la persona restableix la contrasenya mentre `revoke_on_password_change` està activat,
* el teu equip la revoca o bloqueja la persona a la consola,
* es torna a enviar un token d'actualització que ja s'havia utilitzat.

## Durades

| Element | Durada |
| - | - |
| Transacció d'inici de sessió (`POST /authorize/`) | 10 minuts |
| Codi d'autorització | 60 segons, d'un sol ús |
| Token d'accés | 5 minuts, com a màxim |
| Token d'identitat | El mateix `exp` que el token d'accés emès amb ell |
| Token d'actualització | Fins que finalitzi la sessió, d'un sol ús |
| Sessió | `session_lifetime` i `idle_timeout` |
| Pas addicional de verificació | 30 minuts |
| Memòria cau del descobriment i de JWKS | 5 minuts |

## Emmagatzemar les credencials segons la finalitat

* Guarda les claus d'API de verificació i els tokens de la consola al teu servidor.
* Mantín els tokens d'accés del navegador a la memòria sempre que sigui possible. Per a aplicacions renderitzades al servidor, fes servir cookies segures HttpOnly i protegeix les sol·licituds autenticades amb cookies contra CSRF.
* Emmagatzema els tokens d'actualització de les aplicacions mòbils en un emmagatzematge protegit per la plataforma. Esborra l'estat local després de tancar la sessió.
* No hi posis mai tokens a les URL ni als registres.

Continua amb la [guia d'inici ràpid de JavaScript](https://docs.didit.me/ca/authentication/quickstarts/javascript.md), la [guia d'inici ràpid de Next.js](https://docs.didit.me/ca/authentication/quickstarts/nextjs.md) o la [referència de l'API d'autenticació](https://docs.didit.me/ca/authentication/api-reference/get-openid-configuration.md).


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