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

# Проверка JWT и JWKS: Didit Authentication tokens

> Tokens Didit: обмен PKCE, claims ES256, JWKS и discovery для приложения, ротация ключей, обнаружение повторного использования refresh-токена.

> [!WARNING]
>
> Аутентификация появилась недавно. Каждый вызов на этой странице использует учётные данные **конечного пользователя** или не использует их вовсе. Никогда не отправляйте на эти endpoint'ы ваш API-ключ верификации или токен консоли.

Каждое приложение является отдельным эмитентом OpenID Connect. Эмитентом является источник входа вашего приложения:

* `https://<name>.auth.didit.me` для продакшн-приложения,
* `https://<name>.sandbox.auth.didit.me` для приложения в песочнице,
* или ваш собственный домен, после его активации.

Продакшн-среда и песочница используют разные эмитенты и разные ключи. Задайте `DIDIT_AUTH_ISSUER` равным вашему эмитенту без завершающего слеша, а `DIDIT_APPLICATION_ID` равным ID вашего приложения: tokens передают его в `aud`. Ваш публикуемый ключ кодирует эмитент: после `pk_live_` или `pk_test_` идёт эмитент в base64url без дополнения.

> [!NOTE]
>
> Когда ваш собственный домен станет активным, он заменит домен по умолчанию в качестве эмитента. Новые tokens будут указывать его в `iss`, документ обнаружения будет указывать в `jwks_uri` этот домен, а ваш publishable key изменится. Токены доступа, выданные на домене по умолчанию, будут отклоняться в `GET /session/`, пока их не обновят. Обновите `DIDIT_AUTH_ISSUER` при активации домена или принимайте оба эмитента, пока не истекут старые токены, но не дольше 5 минут.

Серверные endpoint'ы этой страницы (discovery, ключи, tokens и сессия) описаны в [справочнике Authentication API](https://docs.didit.me/ru/authentication/api-reference/get-openid-configuration.md), который сформирован из [openapi-authentication.json](https://docs.didit.me/openapi-authentication.json). Размещённая страница входа в этот справочник не входит.

## Discovery и ключи

| Запрос | Результат |
| - | - |
| `GET {issuer}/.well-known/openid-configuration` | `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri` и перечень возможностей, которые поддерживает эмитент. |
| `GET {issuer}/.well-known/jwks.json` | `{"keys": [...]}`, открытые ключи подписи. |

Эмитент поддерживает:

* Тип ответа `code` и режим ответа `query`.
* Типы грантов `authorization_code` и `refresh_token`.
* Только метод PKCE `S256`. Клиенты публичные: client secret отсутствует, а `client_id` равен ID вашего приложения.
* Алгоритм подписи `ES256` (ключи P-256).
* Области действия `openid`, `email`, `phone`, `profile` и `offline_access`.

Endpoint'ов userinfo, introspection и end-session нет, и `prompt=none` не поддерживается. Используйте [`GET /session/`](#проверка-сессии), чтобы считать и проверить сессию.

### Кеширование и ротация

Оба документа отвечают `Cache-Control: public, max-age=300`, поэтому вы и общие кеши можете хранить их 5 минут. Они допускают 1,200 запросов в минуту и 12,000 в час на IP-адрес, что намного больше лимитов endpoint'ов входа, потому что у многих экземпляров серверной части часто бывает один общий адрес.

* У каждого ключа есть `kid`; идентификаторы новых ключей начинаются с `k_`. Каждый token называет свой ключ в заголовке `kid`.
* Ключи меняются каждые 30 дней. Didit публикует следующий ключ за день до начала подписи и держит предыдущий опубликованным ещё день после того, как он перестал подписывать. Поэтому JWKS, закешированный на 5 минут, позволяет проверять токены и во время ротации.
* Кешируйте ключи по `kid`. Если token называет `kid`, которого у вас нет, заново запросите JWKS не чаще раза в минуту и отклоняйте token, если ключ всё ещё отсутствует.
* Когда Didit отзывает скомпрометированный ключ, он сразу удаляет ключ из JWKS, а `GET /session/` отклоняет каждый token, подписанный этим ключом. Замещающий ключ начинает подписывать сразу, без дня предварительной публикации. JWKS, закешированный до 5 минут назад, всё ещё принимает отозванный ключ и пока не знает о замещающем, поэтому новые tokens могут не проходить офлайн-проверку, пока кеш не обновится.

## Начало входа

Откройте размещённую страницу, которая является `authorization_endpoint`:

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

| Параметр | Правило |
| - | - |
| `redirect_uri` | Должен точно совпадать с одним из ваших [зарегистрированных URL перенаправления](https://docs.didit.me/ru/authentication/overview.md#url-перенаправления). |
| `state` | До 512 символов. Проверяйте его в обработчике обратного вызова. |
| `code_challenge` | Base64url от SHA-256 вашего верификатора PKCE: ровно 43 символа. |
| `code_challenge_method` | `S256`. |
| `client_id` | Необязательный. Должен совпадать с ID вашего приложения. |
| `nonce` | Необязательный. Копируется в ID token, чтобы вы могли привязать его к этому входу. |
| `scope`, `login_hint`, `ui_locales`, `max_age`, `prompt` | Необязательные. `login_hint` только предзаполняет поле email. |

[Быстрые старты](https://docs.didit.me/ru/authentication/quickstarts/javascript.md) формируют этот запрос за вас.

## Обмен кода

Callback получает `code` и `state`. Проверьте `state`, затем обменяйте код в течение 60 секунд. Каждый код работает один раз. Тело запроса может быть представлено в формате JSON или в формате данных формы.

```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` показывает, сколько секунд осталось до истечения access token. Если вы отключили возможность нескольких сессий, этот обмен завершает остальные сессии пользователя.

| Ответ | Значение |
| - | - |
| `400 {"error": "invalid_grant"}` | Код неизвестен, уже использован или истёк; верификатор или `redirect_uri` не совпадают, либо `redirect_uri` больше не зарегистрирован; пользователь заблокирован или его email больше не подтверждён; либо вы отключили способ входа, которым он воспользовался. |
| `400 {"error": "invalid_client"}` | `client_id` не равен ID вашего приложения. |
| `400 {"error": "mfa_required"}` | Вход не соответствует вашей политике MFA. |
| `400 {"<field>": [...]}` | Поле отсутствует или некорректно, например `{"non_field_errors": ["invalid_request"]}`, когда в гранте не хватает обязательных полей. |
| `400 {"<field>": "unknown_field"}` | Тело содержит поле, которое endpoint не принимает, например `client_secret`. Публичные клиенты не отправляют секрет клиента. |

## Утверждения access-токена

Access-токен представляет собой JWT с заголовком `alg: ES256` и `typ: at+jwt`. Он действует не более 5 минут и никогда не дольше своей сессии.

| Утверждение | Значение |
| - | - |
| `iss` | Ваш издатель. |
| `aud` | ID вашего приложения. |
| `client_id` | ID вашего приложения. |
| `sub` | Идентификатор личности человека в этом приложении. |
| `sid` | ID сессии. Для токена ИИ-агента это ID доступа, который человек предоставил агенту. |
| `jti`, `iat`, `exp` | ID токена, время выдачи и время истечения. |
| `vendor_data` | Стабильный идентификатор человека, который передаётся в верификацию и мониторинг транзакций. |
| `amr` | Метод входа, например `["passkey"]`. |
| `org` | `{"id", "role"}` самого раннего членства человека в организации вашего приложения или `null`, если у него нет членств или организации отключены. `GET /session/` возвращает все членства. |
| `verified` | Уровень верификации и выбранные вами утверждения или `{}`. Проверка, одобренная во время сессии, попадает в токен при следующем обновлении. |
| `act` | `null` для человека. Для ИИ-агента, действующего от имени человека: `{"sub", "agent", "scopes"}`. См. [шаг 5](#проверка-токена-на-вашем-сервере). |

ID-токен имеет заголовок `typ: JWT`. Он содержит те же утверждения, кроме `client_id`, а также `auth_time` и `email_verified`, и `email`, `name` и `phone_number`, если у них есть значение. Только ID-токен, полученный при обмене кода, содержит `nonce`, если при входе он был передан. Проверяйте его именно там, так как ID-токены, полученные при обновлении, никогда его не содержат. ID-токен никогда не принимается как access-токен.

## Проверка токена на вашем сервере

Проверьте всё перечисленное, прежде чем доверять какому-либо утверждению:

1. `alg` равен `ES256`, а `typ` равен `at+jwt`. Отклоняйте любой другой алгоритм, включая `none`.
2. `kid` есть в JWKS вашего приложения, а подпись успешно проверяется.
3. `iss` совпадает с настроенным издателем, а `aud` совпадает с ID вашего приложения.
4. `exp` и `iat` корректны с допуском до 60 секунд расхождения часов, а `sub`, `sid` и `jti` присутствуют.
5. `act` равен `null`. Access-токен ИИ-агента подписан тем же ключом и имеет те же `typ`, `iss` и `aud`; отличает его только `act`. Отклоняйте его, если только эндпоинт не принимает агентов: тогда разрешайте только то, что разрешено `act.scopes`. `GET /session/` всегда отклоняет токен агента.

Берите издателя из конфигурации, а не из токена.

```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
```

Офлайн-проверка не видит сессию, которая завершилась только что. Когда это важно, например перед выплатой, вызывайте `GET /session/`.

## Проверка сессии

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

Ответ содержит `sub`, `sid`, `application_id`, `vendor_data`, `amr`, `mfa_verified`, `email`, `email_verified`, `phone`, `verified_identity`, `verified`, `expires_at`, `post_logout_redirect_uri` и `organizations`. Каждая запись организации содержит `organization_id`, `organization__name`, `role` и `custom_role`.

Эта проверка строже офлайн-проверки. Она также завершается ошибкой, если сессия была отозвана, простаивала дольше допустимого времени простоя или превысила срок жизни, если человек заблокирован или его адрес электронной почты больше не подтверждён, если сессия больше не соответствует вашей политике MFA (например, после того как человек добавил приложение-аутентификатор или стал администратором организации), или если токен был заменён при обновлении. При ошибке возвращается `401 {"detail": "invalid_token"}` с заголовком `WWW-Authenticate: Bearer`.

`GET /session/` допускает 30 запросов в минуту и 300 в час с одного IP-адреса; этот лимит общий с `DELETE /session/`. Проверяйте токены офлайн при каждом запросе и оставляйте этот вызов для случаев, когда он действительно нужен. Каждый успешный вызов считается активностью и перезапускает `idle_timeout` сессии.

Подтверждённая электронная почта не является верификацией личности. Для этого читайте `verified`: здесь оно равно `null`, если верификации нет, тогда как токены содержат `{}`.

## Обновление токенов

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

Обновление возвращает новый access-токен, ID-токен и refresh-токен, а отправленный вами refresh-токен перестаёт работать.

* Refresh-токен представляет собой случайную строку, которая действительна до окончания сессии.
* **Повторное использование завершает сессию.** Если использованный refresh-токен приходит повторно, Didit расценивает это как признак кражи: сессия отзывается, и возвращается `400 {"error": "invalid_grant"}`. Токены сессии также перестают работать, включая токены, полученные при предыдущем обновлении: их refresh-токен перестаёт работать, а access-токен сразу перестаёт проходить `GET /session/`. Однако офлайн он ещё проверяется до истечения срока, то есть в течение 5 минут.
* Выполняйте одно обновление за раз для сессии, во всех вкладках, на всех устройствах и на всех экземплярах сервера, и заменяйте все три токена одновременно.
* Если запрос обновления завершился ошибкой `400` или на него не пришёл ответ, начните вход заново. Не повторяйте запрос вслепую: повтор после успешного обновления и есть повторное использование.
* Ответ `503 {"error": "temporarily_unavailable"}` означает, что обновление не произошло и ваш refresh-токен всё ещё работает. Повторите запрос с увеличением интервала между попытками.
* Didit возвращает `429` до выполнения обновления, поэтому повторите запрос после `Retry-After`. Эндпоинт токенов допускает 30 запросов в минуту и 300 в час с одного IP-адреса. Кроме того, каждый `POST` и `DELETE` вашего приложения использует общие бакеты защиты от ботов: 60 запросов в минуту с одного IP-адреса на все эти эндпоинты и 30 в минуту с одного устройства. Для всего приложения обновления имеют бакет 6 000 в минуту, а все остальные `POST` и `DELETE` (включая браузеры и шаги входа) имеют бакет 3 000 в минуту, поэтому обновления не исчерпывают лимит входов.
* При 5-минутных access-токенах 300 обновлений в час хватает примерно на 25 активных людей. Если ваш сервер обновляет токены для многих людей с одного адреса, обновляйте только тогда, когда запросу нужен новый токен, и по возможности обновляйте в браузере или приложении.

## Выход из системы

`DELETE /session/` с текущим access token пользователя завершает сессию и возвращает `204`. Её refresh token перестаёт работать, а её access tokens сразу начинают давать ошибку при вызове `GET /session/`. Tokens, которые вы проверяете офлайн, остаются действительными до истечения срока. Затем перенаправьте пользователя на `post_logout_redirect_uri`, если вы его задали.

Для выхода нужен access token, который приняла бы `GET /session/`: он не должен быть просроченным и не должен быть заменён в результате обновления. Ответ `401` означает, что сессия не завершена. Обновите токен, затем выполните выход с новым access token. Если обновление завершается ошибкой `400`, сессию использовать больше нельзя.

Сессия также завершается, когда:

* она превышает `session_lifetime` (по умолчанию 30 дней) или `idle_timeout` (по умолчанию 1 день),
* пользователь входит снова, а несколько сессий отключены,
* пользователь меняет пароль, а `revoke_on_password_change` включён,
* ваша команда отзывает сессию или блокирует пользователя в консоли,
* использованный refresh token отправляется повторно.

## Сроки действия

| Элемент | Срок действия |
| - | - |
| Транзакция входа (`POST /authorize/`) | 10 минут |
| Код авторизации | 60 секунд, одноразовый |
| Access token | не более 5 минут |
| ID-токен | Тот же `exp`, что у выданного с ним access token |
| Refresh token | До завершения сессии, одноразовый |
| Сессия | `session_lifetime` и `idle_timeout` |
| Step-up | 30 минут |
| Кеш discovery и JWKS | 5 минут |

## Хранение учётных данных по назначению

* Храните API-ключи для верификации и tokens консоли на своём сервере.
* По возможности храните access tokens браузера в памяти. В приложениях с серверным рендерингом используйте cookie с флагами Secure и HttpOnly и защищайте запросы с аутентификацией через cookie от CSRF.
* Храните refresh tokens мобильных приложений в защищённом хранилище платформы. После выхода очищайте локальное состояние.
* Никогда не помещайте tokens в URL или журналы.

Перейдите к [быстрому старту для JavaScript](https://docs.didit.me/ru/authentication/quickstarts/javascript.md), [быстрому старту для Next.js](https://docs.didit.me/ru/authentication/quickstarts/nextjs.md) или к [справочнику по Authentication API](https://docs.didit.me/ru/authentication/api-reference/get-openid-configuration.md).


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