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

# Jetons et JWKS : vérifier les JWT d'authentification

> Vérifiez les JWT Didit : échange PKCE, claims ES256, JWKS par application, rotation des clés et détection de réutilisation des refresh tokens.

> [!WARNING]
>
> L'authentification est nouvelle. Chaque appel de cette page utilise des **identifiants d'utilisateur final**, ou aucun identifiant. N'envoyez jamais votre clé API de vérification ni un token de la console à ces endpoints.

Chaque application est son propre émetteur OpenID Connect. L'émetteur correspond à l'origine de connexion de votre application :

* `https://<name>.auth.didit.me` pour une application de production,
* `https://<name>.sandbox.auth.didit.me` pour une application sandbox,
* ou votre propre domaine personnalisé, une fois qu'il est actif.

La production et le sandbox sont des émetteurs distincts, avec des clés distinctes. Définissez `DIDIT_AUTH_ISSUER` avec votre émetteur, sans barre oblique finale, et `DIDIT_APPLICATION_ID` avec l'identifiant de votre application : les tokens le transportent dans `aud`. Votre clé publiable encode l'émetteur : après `pk_live_` ou `pk_test_`, elle contient l'émetteur en base64url sans padding.

> [!NOTE]
>
> Lorsque votre domaine personnalisé devient actif, il remplace l'hôte par défaut comme émetteur. Les nouveaux tokens le mentionnent dans `iss`, la découverte pointe `jwks_uri` vers lui, et votre clé publiable change. Les tokens d'accès émis sous l'hôte par défaut échouent alors sur `GET /session/` jusqu'à leur rafraîchissement. Mettez à jour `DIDIT_AUTH_ISSUER` lorsque vous activez le domaine, ou acceptez les deux émetteurs jusqu'à l'expiration des tokens plus anciens, dans un délai de 5 minutes.

Les endpoints serveur de cette page (découverte, clés, tokens et session) figurent dans la [référence de l'API d'authentification](https://docs.didit.me/fr/authentication/api-reference/get-openid-configuration.md), générée à partir de [openapi-authentication.json](https://docs.didit.me/openapi-authentication.json). La page de connexion hébergée n'y figure pas.

## Découverte et clés

| Requête | Résultat |
| - | - |
| `GET {issuer}/.well-known/openid-configuration` | `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri` et ce que l'émetteur prend en charge. |
| `GET {issuer}/.well-known/jwks.json` | `{"keys": [...]}`, les clés publiques de signature. |

L'émetteur prend en charge :

* Le type de réponse `code` et le mode de réponse `query`.
* Les types d'octroi `authorization_code` et `refresh_token`.
* La méthode PKCE `S256` uniquement. Les clients sont publics : il n'existe pas de secret client, et `client_id` correspond à l'identifiant de votre application.
* L'algorithme de signature `ES256` (clés P-256).
* Les scopes `openid`, `email`, `phone`, `profile` et `offline_access`.

Il n'existe ni endpoint userinfo, ni endpoint d'introspection, ni endpoint de fin de session, et `prompt=none` n'est pas pris en charge. Utilisez [`GET /session/`](#vérifier-la-session) pour lire et vérifier une session.

### Cache et rotation

Les deux documents renvoient `Cache-Control: public, max-age=300`, afin que vous et les caches partagés puissiez les conserver pendant 5 minutes. Ils autorisent 1 200 requêtes par minute et 12 000 par heure et par adresse IP, bien au-delà des endpoints de connexion, car de nombreuses instances backend partagent souvent une même adresse.

* Chaque clé possède un `kid` ; les nouveaux identifiants de clé commencent par `k_`. Chaque token indique sa clé dans l'en-tête `kid`.
* Les clés tournent tous les 30 jours. Didit publie la clé suivante un jour avant qu'elle ne commence à signer, et maintient la clé précédente publiée pendant un jour après qu'elle a cessé de signer. Un JWKS mis en cache pendant 5 minutes permet donc de continuer à vérifier les tokens pendant une rotation.
* Mettez les clés en cache par `kid`. Lorsqu'un token mentionne un `kid` que vous ne possédez pas, récupérez à nouveau le JWKS, au maximum une fois par minute, et rejetez le token si la clé est toujours absente.
* Lorsque Didit révoque une clé compromise, elle retire immédiatement la clé du JWKS, et `GET /session/` rejette chaque token signé par cette clé. Une clé de remplacement commence à signer immédiatement, sans le jour de publication anticipée. Un JWKS mis en cache jusqu'à 5 minutes plus tôt accepte encore la clé révoquée et ne connaît pas encore le remplacement ; les nouveaux tokens peuvent donc échouer à la vérification hors ligne jusqu'au rafraîchissement du cache.

## Démarrer la connexion

Ouvrez la page hébergée, qui correspond à l'`authorization_endpoint` :

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

| Paramètre | Règle |
| - | - |
| `redirect_uri` | Doit correspondre exactement à l'une de vos [URL de redirection enregistrées](https://docs.didit.me/fr/authentication/overview.md#url-de-redirection). |
| `state` | Jusqu'à 512 caractères. Vérifiez-le lors du rappel. |
| `code_challenge` | Le SHA-256 en base64url de votre vérificateur PKCE : exactement 43 caractères. |
| `code_challenge_method` | `S256`. |
| `client_id` | Facultatif. Doit être égal à l'identifiant de votre application. |
| `nonce` | Facultatif. Copié dans le token d'identité, afin que vous puissiez le lier à cette connexion. |
| `scope`, `login_hint`, `ui_locales`, `max_age`, `prompt` | Facultatifs. `login_hint` pré-remplit uniquement le champ e-mail. |

Les [guides de démarrage rapide](https://docs.didit.me/fr/authentication/quickstarts/javascript.md) construisent cette requête à votre place.

## Échanger le code

Le callback reçoit `code` et `state`. Vérifiez `state`, puis échangez le code dans les 60 secondes. Chaque code ne fonctionne qu'une fois. Le corps peut être au format JSON ou encodé en formulaire.

```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` indique le nombre de secondes restantes avant l'expiration du token d'accès. Lorsque vous désactivez les sessions multiples, cet échange met fin aux autres sessions de la personne.

| Réponse | Signification |
| - | - |
| `400 {"error": "invalid_grant"}` | Le code est inconnu, déjà utilisé ou expiré ; le vérificateur ou le `redirect_uri` ne correspond pas, ou le `redirect_uri` n'est plus enregistré ; la personne est bloquée ou son e-mail n'est plus vérifié ; ou vous avez désactivé la méthode de connexion qu'elle a utilisée. |
| `400 {"error": "invalid_client"}` | `client_id` n'est pas l'identifiant de votre application. |
| `400 {"error": "mfa_required"}` | La connexion ne respecte pas votre politique MFA. |
| `400 {"<field>": [...]}` | Un champ est manquant ou invalide, par exemple `{"non_field_errors": ["invalid_request"]}` lorsqu'un octroi ne contient pas ses champs requis. |
| `400 {"<field>": "unknown_field"}` | Le corps contient un champ que l'endpoint n'accepte pas, par exemple `client_secret`. Les clients publics n'en envoient aucun. |

## Revendications du jeton d'accès

Le jeton d'accès est un JWT dont l'en-tête est `alg: ES256` et `typ: at+jwt`. Il est valable 5 minutes au maximum, et jamais plus longtemps que sa session.

| Revendication | Signification |
| - | - |
| `iss` | Votre émetteur. |
| `aud` | L'ID de votre application. |
| `client_id` | L'ID de votre application. |
| `sub` | L'identifiant d'identité de la personne dans cette application. |
| `sid` | L'ID de la session. Pour le jeton d'un agent IA, l'ID de l'accès que la personne a accordé à l'agent. |
| `jti`, `iat`, `exp` | ID du jeton, date d'émission et date d'expiration. |
| `vendor_data` | La référence stable de la personne, partagée avec la vérification et la surveillance des transactions. |
| `amr` | La méthode de connexion, par exemple `["passkey"]`. |
| `org` | `{"id", "role"}` de la première appartenance de la personne à une organisation de votre application, ou `null` si elle n'en a aucune ou si les organisations sont désactivées. `GET /session/` liste toutes les appartenances. |
| `verified` | Le niveau de vérification et les revendications que vous avez sélectionnées, ou `{}`. Une vérification approuvée pendant une session atteint le jeton lors du prochain rafraîchissement. |
| `act` | `null` pour une personne. Pour un agent IA agissant au nom de la personne : `{"sub", "agent", "scopes"}`. Voir [étape 5](#vérifier-un-jeton-sur-votre-serveur). |

Le jeton ID a l'en-tête `typ: JWT`. Il contient les mêmes revendications, à l'exception de `client_id`, ainsi que `auth_time` et `email_verified`, et `email`, `name` et `phone_number` lorsqu'ils ont une valeur. Seul le jeton ID issu de l'échange du code contient `nonce`, lorsque la connexion en comportait un : vérifiez-le à cet endroit, car les jetons ID issus d'un rafraîchissement ne le contiennent jamais. Un jeton ID n'est jamais accepté comme jeton d'accès.

## Vérifier un jeton sur votre serveur

Vérifiez tous les points suivants avant de faire confiance à une revendication :

1. `alg` vaut `ES256` et `typ` vaut `at+jwt`. Rejetez tout autre algorithme, y compris `none`.
2. Le `kid` figure dans le JWKS de votre application et la signature est valide.
3. `iss` est égal à votre émetteur configuré, et `aud` est égal à l'ID de votre application.
4. `exp` et `iat` sont valides, avec une tolérance d'écart d'horloge de 60 secondes au maximum, et `sub`, `sid` et `jti` sont présents.
5. `act` vaut `null`. Le jeton d'accès d'un agent IA est signé avec la même clé et porte les mêmes `typ`, `iss` et `aud` ; seul `act` permet de le distinguer. Rejetez-le, sauf si l'endpoint accepte les agents : dans ce cas, n'autorisez que ce que `act.scopes` accorde. `GET /session/` rejette toujours le jeton d'un agent.

Prenez l'émetteur depuis votre configuration, jamais depuis le jeton.

```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 vérification hors ligne ne peut pas détecter une session qui s'est terminée à l'instant. Lorsque cela importe, par exemple avant un versement, appelez `GET /session/`.

## Vérifier la session

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

La réponse contient `sub`, `sid`, `application_id`, `vendor_data`, `amr`, `mfa_verified`, `email`, `email_verified`, `phone`, `verified_identity`, `verified`, `expires_at`, `post_logout_redirect_uri` et `organizations`. Chaque entrée d'organisation contient `organization_id`, `organization__name`, `role` et `custom_role`.

Cette vérification est plus stricte que la vérification hors ligne. Elle échoue aussi lorsque la session a été révoquée, est restée inactive ou a dépassé sa durée de vie, lorsque la personne a été bloquée ou que son e-mail n'est plus vérifié, lorsque la session ne respecte plus votre politique MFA (par exemple après l'ajout d'une application d'authentification par la personne ou sa nomination comme administrateur d'une organisation), ou lorsque le jeton a été remplacé par un rafraîchissement. Un échec renvoie `401 {"detail": "invalid_token"}` avec `WWW-Authenticate: Bearer`.

`GET /session/` autorise 30 requêtes par minute et 300 par heure et par adresse IP, une limite qu'il partage avec `DELETE /session/`. Vérifiez les jetons hors ligne à chaque requête, et réservez cet appel aux moments qui en ont besoin. Chaque appel réussi compte comme une activité : il réinitialise le `idle_timeout` de la session.

Un e-mail vérifié n'est pas une vérification d'identité. Lisez `verified` pour cela : ici, il vaut `null` lorsqu'il n'y en a pas, alors que les jetons contiennent `{}`.

## Rafraîchir les jetons

```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 rafraîchissement renvoie un nouveau jeton d'accès, un nouveau jeton ID et un nouveau jeton de rafraîchissement, et le jeton de rafraîchissement que vous avez envoyé cesse de fonctionner.

* Le jeton de rafraîchissement est une chaîne aléatoire, valable jusqu'à la fin de la session.
* **La réutilisation met fin à la session.** Lorsqu'un jeton de rafraîchissement déjà utilisé est à nouveau reçu, Didit y voit le signe d'un vol : il révoque la session et renvoie `400 {"error": "invalid_grant"}`. Les jetons de la session cessent aussi de fonctionner, y compris ceux du rafraîchissement précédent : leur jeton de rafraîchissement échoue, et leur jeton d'accès échoue immédiatement sur `GET /session/`, bien qu'il reste valable hors ligne jusqu'à son expiration, dans un délai de 5 minutes.
* Ne lancez qu'un seul rafraîchissement à la fois pour une session, entre onglets, appareils et instances de serveur, et remplacez les trois jetons ensemble.
* Si un rafraîchissement échoue avec `400` ou n'obtient aucune réponse, relancez la connexion. Ne réessayez pas aveuglément : une nouvelle tentative après un rafraîchissement réussi constitue une réutilisation.
* Un `503 {"error": "temporarily_unavailable"}` signifie que le rafraîchissement n'a pas eu lieu et que votre jeton de rafraîchissement fonctionne toujours. Réessayez avec un délai progressif.
* Didit répond par un `429` avant l'exécution du rafraîchissement : réessayez après `Retry-After`. L'endpoint de jetons autorise 30 requêtes par minute et 300 par heure et par adresse IP. En plus, chaque `POST` et `DELETE` de votre application partage des compartiments de protection contre les bots : 60 requêtes par minute et par adresse IP pour l'ensemble de ces endpoints, et 30 par minute et par appareil. Pour l'ensemble de l'application, les rafraîchissements disposent d'un compartiment de 6 000 par minute, et chaque autre `POST` et `DELETE` (navigateurs et étapes de connexion compris) d'un compartiment de 3 000 par minute, afin que les rafraîchissements n'épuisent jamais votre quota de connexions.
* Avec des jetons d'accès de 5 minutes, 300 rafraîchissements par heure suffisent pour environ 25 personnes restant actives. Si votre serveur rafraîchit les jetons de nombreuses personnes depuis une même adresse, ne rafraîchissez que lorsqu'une requête a besoin d'un nouveau jeton, et rafraîchissez dans le navigateur ou l'application lorsque c'est possible.

## Déconnexion

`DELETE /session/` avec le token d'accès actuel de la personne met fin à la session et renvoie `204`. Son token de renouvellement cesse de fonctionner, et ses tokens d'accès échouent immédiatement lors de l'appel à `GET /session/`. Les tokens que vous vérifiez hors ligne restent valides jusqu'à leur expiration. Redirigez ensuite la personne vers votre `post_logout_redirect_uri`, si vous en avez défini un.

La déconnexion nécessite un token d'accès accepté par `GET /session/` : un token non expiré et qui n'a pas été remplacé lors d'un renouvellement. Un `401` signifie que la session n'a pas pris fin. Renouvelez le token, puis déconnectez-vous avec le nouveau token d'accès. Si le renouvellement échoue avec `400`, la session ne peut plus être utilisée.

Une session prend également fin lorsque :

* elle dépasse `session_lifetime` (30 jours par défaut) ou `idle_timeout` (1 jour par défaut),
* la personne se reconnecte alors que les sessions multiples sont désactivées,
* la personne réinitialise son mot de passe alors que `revoke_on_password_change` est activé,
* votre équipe la révoque ou bloque la personne dans la console,
* un token de renouvellement déjà utilisé est envoyé à nouveau.

## Durées de vie

| Élément | Durée de vie |
| - | - |
| Transaction de connexion (`POST /authorize/`) | 10 minutes |
| Code d'autorisation | 60 secondes, usage unique |
| Token d'accès | 5 minutes maximum |
| ID token | Même `exp` que le token d'accès émis avec lui |
| Token de renouvellement | Jusqu'à la fin de la session, usage unique |
| Session | `session_lifetime` et `idle_timeout` |
| Authentification renforcée | 30 minutes |
| Cache de la découverte et du JWKS | 5 minutes |

## Stocker les identifiants selon leur usage

* Conservez les clés API de vérification et les tokens de la console sur votre serveur.
* Conservez les tokens d'accès du navigateur en mémoire lorsque c'est possible. Pour les applications rendues côté serveur, utilisez des cookies sécurisés HttpOnly et protégez les requêtes authentifiées par cookie contre les attaques CSRF.
* Stockez les tokens de renouvellement mobiles dans un stockage protégé par la plateforme. Effacez l'état local après la déconnexion.
* Ne placez jamais de tokens dans des URL ou dans des journaux.

Poursuivez avec le [Démarrage rapide JavaScript](https://docs.didit.me/fr/authentication/quickstarts/javascript.md), le [Démarrage rapide Next.js](https://docs.didit.me/fr/authentication/quickstarts/nextjs.md) ou la [Référence de l'API d'authentification](https://docs.didit.me/fr/authentication/api-reference/get-openid-configuration.md).


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