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

# Token e JWKS: verifica dei JWT di autenticazione Didit

> Token di autenticazione Didit: scambio PKCE, claim ES256, discovery e JWKS per applicazione, rotazione delle chiavi, riutilizzo dei refresh token.

> [!WARNING]
>
> L'autenticazione è una novità. Ogni chiamata in questa pagina usa credenziali dell'**utente finale** oppure nessuna credenziale. Non inviare mai la tua chiave API di verifica né un token della console a questi endpoint.

Ogni applicazione è un proprio issuer OpenID Connect. L'issuer è l'origine dell'accesso alla tua applicazione:

* `https://<name>.auth.didit.me` per un'applicazione live,
* `https://<name>.sandbox.auth.didit.me` per un'applicazione sandbox,
* oppure il tuo dominio personalizzato, una volta attivo.

Live e sandbox sono issuer separati, con chiavi separate. Imposta `DIDIT_AUTH_ISSUER` con il tuo issuer, senza barra finale, e `DIDIT_APPLICATION_ID` con l'ID della tua applicazione: i token lo riportano nel claim `aud`. La tua chiave pubblicabile codifica l'issuer: dopo `pk_live_` o `pk_test_` c'è l'issuer in base64url senza padding.

> [!NOTE]
>
> Quando il tuo dominio personalizzato diventa attivo, sostituisce l'host predefinito come issuer. I nuovi token lo indicano in `iss`, il discovery fa puntare `jwks_uri` ad esso e la tua chiave pubblicabile cambia. I token di accesso emessi sotto l'host predefinito falliscono su `GET /session/` finché non vengono aggiornati. Aggiorna `DIDIT_AUTH_ISSUER` quando attivi il dominio, oppure accetta entrambi gli issuer finché i token più vecchi non scadono, entro 5 minuti.

Gli endpoint server di questa pagina (discovery, chiavi, token e sessione) sono nel [Riferimento API di autenticazione](https://docs.didit.me/it/authentication/api-reference/get-openid-configuration.md), generato da [openapi-authentication.json](https://docs.didit.me/openapi-authentication.json). La pagina di accesso ospitata non lo è.

## Discovery e chiavi

| Richiesta | Risultato |
| - | - |
| `GET {issuer}/.well-known/openid-configuration` | `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri` e ciò che l'issuer supporta. |
| `GET {issuer}/.well-known/jwks.json` | `{"keys": [...]}`, le chiavi pubbliche di firma. |

L'issuer supporta:

* Tipo di risposta `code` e modalità di risposta `query`.
* Tipi di grant `authorization_code` e `refresh_token`.
* Solo il metodo PKCE `S256`. I client sono pubblici: non esiste alcun client secret, e `client_id` è l'ID della tua applicazione.
* Algoritmo di firma `ES256` (chiavi P-256).
* Scope `openid`, `email`, `phone`, `profile` e `offline_access`.

Non esistono endpoint userinfo, introspection o end-session, e `prompt=none` non è supportato. Usa [`GET /session/`](#controlla-la-sessione) per leggere e verificare una sessione.

### Cache e rotazione

Entrambi i documenti rispondono con `Cache-Control: public, max-age=300`, quindi tu e le cache condivise potete conservarli per 5 minuti. Consentono 1.200 richieste al minuto e 12.000 l'ora per indirizzo, molto al di sopra degli endpoint di accesso, perché spesso molte istanze backend condividono un solo indirizzo.

* Ogni chiave ha un `kid`; i nuovi ID di chiave iniziano con `k_`. Ogni token indica la propria chiave nell'header `kid`.
* Le chiavi ruotano ogni 30 giorni. Didit pubblica la chiave successiva un giorno prima che inizi a firmare e mantiene pubblicata la chiave precedente per un giorno dopo che ha smesso di firmare. Un JWKS in cache per 5 minuti continua quindi a verificare anche durante una rotazione.
* Metti in cache le chiavi per `kid`. Quando un token indica un `kid` che non hai, richiedi di nuovo il JWKS, al massimo una volta al minuto, e rifiuta il token se la chiave manca ancora.
* Quando Didit revoca una chiave compromessa, la rimuove subito dal JWKS e `GET /session/` rifiuta ogni token firmato da quella chiave. Una chiave sostitutiva inizia a firmare immediatamente, senza il giorno di pubblicazione anticipata. Un JWKS in cache fino a 5 minuti prima accetta ancora la chiave revocata e non conosce ancora quella sostitutiva, quindi i nuovi token possono non superare la verifica offline finché la cache non si aggiorna.

## Avvia l'accesso

Apri la pagina ospitata, cioè l'`authorization_endpoint`:

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

| Parametro | Regola |
| - | - |
| `redirect_uri` | Deve corrispondere esattamente a uno dei tuoi [URL di reindirizzamento registrati](https://docs.didit.me/it/authentication/overview.md#url-di-reindirizzamento). |
| `state` | Fino a 512 caratteri. Verificalo nel callback. |
| `code_challenge` | Lo SHA-256 in base64url del tuo codice di verifica PKCE: esattamente 43 caratteri. |
| `code_challenge_method` | `S256`. |
| `client_id` | Facoltativo. Deve coincidere con l'ID della tua applicazione. |
| `nonce` | Facoltativo. Viene copiato nell'ID token, così puoi legarlo a questo accesso. |
| `scope`, `login_hint`, `ui_locales`, `max_age`, `prompt` | Facoltativi. `login_hint` precompila solo il campo email. |

Le [guide rapide](https://docs.didit.me/it/authentication/quickstarts/javascript.md) costruiscono questa richiesta per te.

## Scambia il codice

Il callback riceve `code` e `state`. Verifica `state`, poi scambia il codice entro 60 secondi. Ogni codice funziona una sola volta. Il corpo può essere JSON oppure codificato in formato form.

```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 i secondi che restano al token di accesso. Quando disattivi le sessioni multiple, questo scambio termina le altre sessioni della persona.

| Risposta | Significato |
| - | - |
| `400 {"error": "invalid_grant"}` | Il codice è sconosciuto, già usato o scaduto; il verificatore o il `redirect_uri` non corrispondono, oppure il `redirect_uri` non è più registrato; la persona è bloccata o la sua email non è più verificata; oppure hai disattivato il metodo di accesso che ha usato. |
| `400 {"error": "invalid_client"}` | `client_id` non è l'ID della tua applicazione. |
| `400 {"error": "mfa_required"}` | L'accesso non soddisfa la tua politica MFA. |
| `400 {"<field>": [...]}` | Un campo manca o non è valido, per esempio `{"non_field_errors": ["invalid_request"]}` quando un grant non ha i campi obbligatori. |
| `400 {"<field>": "unknown_field"}` | Il corpo contiene un campo che l'endpoint non accetta, per esempio `client_secret`. I client pubblici non ne inviano. |

## Claim del token di accesso

Il token di accesso è un JWT con intestazione `alg: ES256` e `typ: at+jwt`. Dura al massimo 5 minuti e mai più a lungo della sessione.

| Claim | Significato |
| - | - |
| `iss` | Il tuo issuer. |
| `aud` | L'ID della tua applicazione. |
| `client_id` | L'ID della tua applicazione. |
| `sub` | L'ID dell'identità della persona in questa applicazione. |
| `sid` | L'ID della sessione. Per il token di un agente AI, l'ID dell'accesso che la persona ha concesso all'agente. |
| `jti`, `iat`, `exp` | ID del token, ora di emissione e scadenza. |
| `vendor_data` | Il riferimento stabile della persona, condiviso con la verifica e il monitoraggio delle transazioni. |
| `amr` | Il metodo di accesso, per esempio `["passkey"]`. |
| `org` | `{"id", "role"}` dell'appartenenza più risalente della persona a un'organizzazione nella tua applicazione, oppure `null` se non ne ha o se le organizzazioni sono disattivate. `GET /session/` elenca tutte le appartenenze. |
| `verified` | Il livello di verifica e i claim che hai selezionato, oppure `{}`. Un controllo approvato durante una sessione arriva al token al successivo refresh. |
| `act` | `null` per una persona. Per un agente AI che agisce per conto della persona: `{"sub", "agent", "scopes"}`. Vedi [passaggio 5](#verifica-un-token-sul-tuo-server). |

Il token ID ha intestazione `typ: JWT`. Contiene gli stessi claim, tranne `client_id`, più `auth_time` ed `email_verified`, e `email`, `name` e `phone_number` quando hanno un valore. Solo il token ID dello scambio del codice contiene `nonce`, se l'accesso ne prevedeva uno: verificalo lì, perché i token ID dei refresh non lo contengono mai. Un token ID non viene mai accettato come token di accesso.

## Verifica un token sul tuo server

Controlla tutti questi punti prima di fidarti di un claim:

1. `alg` è `ES256` e `typ` è `at+jwt`. Rifiuta ogni altro algoritmo, incluso `none`.
2. Il `kid` è presente nel JWKS della tua applicazione e la firma viene verificata.
3. `iss` coincide con l'issuer configurato e `aud` coincide con l'ID della tua applicazione.
4. `exp` e `iat` sono validi, con una differenza di orologio fino a 60 secondi, e `sub`, `sid` e `jti` sono presenti.
5. `act` è `null`. Il token di accesso di un agente AI è firmato con la stessa chiave e ha gli stessi `typ`, `iss` e `aud`; solo `act` lo distingue. Rifiutalo, a meno che l'endpoint accetti agenti: in quel caso consenti solo ciò che `act.scopes` concede. `GET /session/` rifiuta sempre il token di un agente.

Prendi l'issuer dalla configurazione, mai dal 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 verifica offline non può vedere una sessione terminata da poco. Quando questo conta, per esempio prima di un pagamento, chiama `GET /session/`.

## Controlla la sessione

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

La risposta contiene `sub`, `sid`, `application_id`, `vendor_data`, `amr`, `mfa_verified`, `email`, `email_verified`, `phone`, `verified_identity`, `verified`, `expires_at`, `post_logout_redirect_uri` e `organizations`. Ogni voce di organizzazione contiene `organization_id`, `organization__name`, `role` e `custom_role`.

Questo controllo è più rigoroso della verifica offline. Fallisce anche quando la sessione è stata revocata, è rimasta inattiva o ha superato la sua durata massima, quando la persona è stata bloccata o la sua email non è più verificata, quando la sessione non soddisfa più la tua politica MFA (per esempio dopo che la persona ha aggiunto un'app di autenticazione o è diventata amministratrice di un'organizzazione), o quando il token è stato sostituito da un refresh. Un errore restituisce `401 {"detail": "invalid_token"}` con `WWW-Authenticate: Bearer`.

`GET /session/` consente 30 richieste al minuto e 300 all'ora per indirizzo, un limite che condivide con `DELETE /session/`. Verifica i token offline a ogni richiesta e riserva questa chiamata ai momenti che ne hanno bisogno. Ogni chiamata riuscita conta come attività e quindi riavvia l'`idle_timeout` della sessione.

Un'email verificata non è una verifica dell'identità. Per quella leggi `verified`: qui è `null` se non c'è, mentre i token contengono `{}`.

## Refresh token

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

Un refresh restituisce un nuovo token di accesso, un nuovo token ID e un nuovo refresh token, e il refresh token che hai inviato smette di funzionare.

* Il refresh token è una stringa casuale, valida fino al termine della sessione.
* **Il riutilizzo termina la sessione.** Quando arriva di nuovo un refresh token già usato, Didit lo considera un segnale di furto: revoca la sessione e restituisce `400 {"error": "invalid_grant"}`. Anche i token della sessione smettono di funzionare, compresi quelli del refresh precedente: il loro refresh token fallisce e il loro token di accesso fallisce subito in `GET /session/`, anche se la verifica offline resta valida fino alla scadenza, entro 5 minuti.
* Esegui un solo refresh alla volta per sessione, tra schede, dispositivi e istanze del server, e sostituisci tutti e tre i token insieme.
* Se un refresh fallisce con `400` o non riceve risposta, avvia di nuovo l'accesso. Non ritentarlo alla cieca: un nuovo tentativo dopo un refresh andato a buon fine è un riutilizzo.
* Un `503 {"error": "temporarily_unavailable"}` significa che il refresh non è avvenuto e il tuo refresh token funziona ancora. Ritenta con backoff.
* Didit risponde con un `429` prima di eseguire il refresh, quindi ritenta dopo `Retry-After`. L'endpoint dei token consente 30 richieste al minuto e 300 all'ora per indirizzo. In aggiunta, ogni `POST` e `DELETE` della tua applicazione condivide i bucket di protezione dai bot: 60 richieste al minuto per indirizzo su questi endpoint e 30 al minuto per dispositivo. Per l'intera applicazione, i refresh hanno un bucket di 6.000 al minuto e ogni altro `POST` e `DELETE` (browser e passaggi di accesso inclusi) ne ha uno di 3.000 al minuto, così i refresh non esauriscono i tuoi accessi.
* Con token di accesso da 5 minuti, 300 refresh all'ora coprono circa 25 persone che rimangono attive. Se il tuo server esegue refresh per molte persone dallo stesso indirizzo, esegui il refresh solo quando una richiesta ha bisogno di un nuovo token, e fallo nel browser o nell'app dove è possibile.

## Disconnessione

`DELETE /session/` con l'access token corrente della persona termina la sessione e restituisce `204`. Il suo refresh token smette di funzionare e i suoi access token non superano più `GET /session/` immediatamente; i token che verifichi offline restano validi fino alla scadenza. Poi reindirizza la persona al tuo `post_logout_redirect_uri`, se ne hai impostato uno.

La disconnessione richiede un access token che `GET /session/` accetterebbe: uno non scaduto e non sostituito da un refresh. Un `401` significa che la sessione non è terminata. Esegui un aggiornamento, poi disconnetti con il nuovo access token; se l'aggiornamento fallisce con `400`, la sessione non può più essere usata.

Una sessione termina anche quando:

* supera `session_lifetime` (predefinito 30 giorni) o `idle_timeout` (predefinito 1 giorno),
* la persona accede di nuovo mentre le sessioni multiple sono disattivate,
* la persona reimposta la password mentre `revoke_on_password_change` è attivo,
* il tuo team la revoca o blocca la persona nella console,
* un refresh token già usato viene inviato di nuovo.

## Durate

| Elemento | Durata |
| - | - |
| Transazione di accesso (`POST /authorize/`) | 10 minuti |
| Codice di autorizzazione | 60 secondi, uso singolo |
| Access token | 5 minuti, al massimo |
| ID token | Stesso `exp` dell'access token emesso con esso |
| Refresh token | Fino al termine della sessione, uso singolo |
| Sessione | `session_lifetime` e `idle_timeout` |
| Step-up | 30 minuti |
| Cache di discovery e JWKS | 5 minuti |

## Conservare le credenziali in base alla finalità

* Conserva le chiavi API di verifica e i token della console sul tuo server.
* Conserva gli access token del browser in memoria, dove possibile. Per le app renderizzate lato server, usa cookie sicuri con HttpOnly e proteggi le richieste autenticate tramite cookie dagli attacchi CSRF.
* Conserva i refresh token per dispositivi mobili in un'archiviazione protetta dalla piattaforma. Cancella lo stato locale dopo la disconnessione.
* Non inserire mai i token negli URL o nei log.

Continua con [avvio rapido con JavaScript](https://docs.didit.me/it/authentication/quickstarts/javascript.md), [avvio rapido con Next.js](https://docs.didit.me/it/authentication/quickstarts/nextjs.md) oppure [riferimento dell'API di autenticazione](https://docs.didit.me/it/authentication/api-reference/get-openid-configuration.md).


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