> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aisevenai.ru/llms.txt
> Use this file to discover all available pages before exploring further.

# Виджет

> Ваш виджет в кабинете Ai-Seven: ссылка с токеном, проверка токена, JWKS

Виджет — ваша страница, которую мы открываем внутри кабинета Ai-Seven: например, рейтинг с подсвеченной строкой пользователя и кнопкой «Купить участие». Адрес виджета и название пункта меню мы заводим в настройках вашей интеграции.

Чтобы вы знали, кто открыл виджет, мы добавляем к адресу подписанный нами токен с `public_id` пользователя. Вы проверяете подпись нашим публичным ключом — так вы знаете, что ссылку сформировали мы и `public_id` не подменён. Но токен предъявительский: подпись не доказывает, что ссылку открыл тот, кому мы её выдали, — см. [Кто открыл ссылку](#кто-открыл-ссылку).

## Подключение

**1. Вы присылаете адрес виджета и название пункта меню**

Адрес страницы виджета — только `https://` и на вашем домене, не на домене Ai-Seven. Название пункта меню в кабинете Ai-Seven — до 100 символов.

**2. Мы сообщаем значение aud**

Это ваш `iss` в Partner API — идентификатор вашей интеграции. С ним вы сверяете `aud` каждого токена.

## Как это работает

**1. Пользователь открывает страницу виджета**

В кабинете Ai-Seven — пункт меню с названием вашего виджета.

**2. Даёт согласие**

При первом открытии — на передачу данных вам, см. [Согласие и профиль](https://docs.aisevenai.ru/partner-api/profile.md). Без согласия ссылка с токеном не выдаётся, и `public_id` к вам не уходит.

**3. Мы открываем виджет с токеном**

По адресу `<адрес виджета>?token=<JWT>` — во фрейме кабинета или, по кнопке «Открыть в новой вкладке», в отдельной вкладке. Токен выпускается заново при каждом открытии.

**4. Вы проверяете токен**

Подпись, срок, адресата и одноразовость — и обмениваете токен на свою сессию. Токен срабатывает у любого, у кого окажется ссылка, поэтому показывайте пользователю, под кем он вошёл.

## Ссылка

```text theme={null}
https://partner.example.com/widget?token=eyJhbGciOiJSUzI1NiIsImtpZCI6Ij...
```

Токен передаётся в параметре `token`. Если в адресе виджета уже есть параметры, токен добавляется к ним через `&`: `https://partner.example.com/widget?lang=ru&token=...`.

## Формат токена

### Заголовок

```json theme={null}
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
}
```

**alg** `string` *required*

Всегда `RS256`.

**kid** `string` *required*

Идентификатор нашего ключа подписи. По нему вы выбираете ключ из [JWKS](#jwks).

### Payload

```json theme={null}
{
  "iss": "https://aisevenai.ru",
  "aud": "<идентификатор вашей интеграции>",
  "sub": "9f1c2a6e-4b70-4f1a-9d2c-8b5e0a7d3f42",
  "iat": 1761379200,
  "exp": 1761379500,
  "jti": "3f0c1d9a6b2e4f7a8c5d1e0b9a7f6c4d"
}
```

**iss** `string` *required*

Всегда `https://aisevenai.ru`.

**aud** `string` *required*

Ваш `iss` в Partner API — идентификатор вашей интеграции, который мы сообщаем при подключении. Токен, выпущенный для другого партнёра, вам не подходит.

**sub** `string` *required*

`public_id` пользователя — тот же, что в [статистике](https://docs.aisevenai.ru/partner-api/statistics.md). По нему вы [выдаёте подписку](https://docs.aisevenai.ru/partner-api/subscriptions.md) и [доступы](https://docs.aisevenai.ru/partner-api/accesses.md).

**iat** `integer` *required*

Момент выпуска, unix-время в секундах.

**exp** `integer` *required*

Момент истечения, unix-время в секундах. Токен живёт 300 секунд.

**jti** `string` *required*

Уникальный идентификатор токена. Нужен для одноразовости.

## JWKS

Наши публичные ключи — по фиксированному адресу, без авторизации:

```http theme={null}
GET https://aisevenai.ru/api/v1/partner/.well-known/jwks.json
```

Это пример формата. Ключ берите только запросом по адресу выше, в код не вставляйте.

```json Пример theme={null}
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs",
      "use": "sig",
      "alg": "RS256",
      "n": "…",
      "e": "AQAB"
    }
  ]
}
```

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

**1. Подпись — только RS256**

Список допустимых алгоритмов задайте жёстко: `["RS256"]`. Не берите алгоритм из заголовка токена.

**2. Ключ — только из нашего JWKS**

Загружайте JWKS только с фиксированного адреса `https://aisevenai.ru/api/v1/partner/.well-known/jwks.json`. Не стройте адрес ключей из `iss`, `jku` или `x5u` токена.

**3. iss и aud — точное совпадение**

`iss` — точно `https://aisevenai.ru`, `aud` — точно ваш `iss` в Partner API.

**4. Срок**

Проверяйте `exp` и `iat` с допуском на расхождение часов 30–60 секунд. Токен живёт 300 секунд.

**5. Кеш JWKS и незнакомый kid**

Держите JWKS в кеше не дольше 10 минут. Если `kid` нет в закешированном JWKS — перечитайте JWKS, но не чаще раза в минуту.

**6. Одноразовость по jti**

Храните `jti` принятого токена до его `exp` плюс допуск по часам и записывайте атомарно — например, в Redis: `SET widget-jti:<jti> 1 NX EXAT <exp+60>`. Не записалось — токен уже предъявляли: отклоните его.

**7. Сразу обменяйте токен на свою сессию**

Главный способ — обмен на сервере. Ваш сервер принимает `GET <адрес виджета>?token=…`, проверяет токен, ставит сессию и отвечает `303` на тот же адрес без `token`. Так токен не видят скрипты страницы и он не уходит в `Referer`.

Одностраничное приложение, которое получает токен в браузере, отправляет его на свой сервер в теле `POST`, а не в адресе, и сразу убирает из адреса только параметр `token`, сохраняя остальные:

```javascript theme={null}
const url = new URL(location.href);
url.searchParams.delete("token");
history.replaceState(null, "", url);
```

> [!WARNING]
>
> `sub` (`public_id`) — **не доказательство входа** сам по себе. Опознавайте пользователя только по проверенному токену, а не по `public_id` из адреса, формы или вашей базы.

### Кто открыл ссылку

Токен виджета — предъявительский. Подпись доказывает, что ссылку выпустили мы для этого `public_id`, но не то, что её открыл тот, кому мы её выдали: пересланная или перехваченная ссылка до `exp` сработает и у другого человека. Поэтому:

* Показывайте на странице виджета, под кем выполнен вход.
* Если в браузере уже открыта ваша сессия другого `sub`, не переключайте её молча: покажите, что пришёл вход под другим пользователем, и спросите, под кем продолжить.
* При входе по ссылке в отдельной вкладке, а не во фрейме, просите подтверждение: «Продолжить как `<имя>`». На сервере отличайте фрейм от вкладки по заголовку `Sec-Fetch-Dest` (`iframe` или `document`); в браузере — по `window.self === window.top`.
* Проверка `Referer` — только дополнение к этим мерам, а не замена им.

### Пример

```python Python theme={null}
import time
import jwt  # pip install pyjwt[crypto]
import requests

JWKS_URL = "https://aisevenai.ru/api/v1/partner/.well-known/jwks.json"
ISSUER = "https://aisevenai.ru"
AUDIENCE = "<идентификатор вашей интеграции>"

JWKS_MAX_AGE = 600
UNKNOWN_KID_COOLDOWN = 60

_keys: dict[str, jwt.PyJWK] = {}
_fetched_at = 0.0


def _key(kid: str) -> jwt.PyJWK:
    global _keys, _fetched_at
    age = time.time() - _fetched_at
    if age >= JWKS_MAX_AGE or (kid not in _keys and age >= UNKNOWN_KID_COOLDOWN):
        jwks = requests.get(JWKS_URL, timeout=5).json()
        _keys = {k["kid"]: jwt.PyJWK(k) for k in jwks["keys"]}
        _fetched_at = time.time()
    return _keys[kid]


def verify_widget_token(token: str) -> dict:
    kid = jwt.get_unverified_header(token)["kid"]
    claims = jwt.decode(
        token,
        _key(kid).key,
        algorithms=["RS256"],
        issuer=ISSUER,
        audience=AUDIENCE,
        leeway=60,
        options={"require": ["iss", "aud", "sub", "iat", "exp", "jti"]},
    )
    remember_jti_or_reject(claims["jti"], until=claims["exp"] + 60)
    return claims
```

```javascript Node.js theme={null}
import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS = createRemoteJWKSet(
  new URL("https://aisevenai.ru/api/v1/partner/.well-known/jwks.json"),
  { cooldownDuration: 60_000, cacheMaxAge: 600_000 },
);

export async function verifyWidgetToken(token) {
  const { payload } = await jwtVerify(token, JWKS, {
    algorithms: ["RS256"],
    issuer: "https://aisevenai.ru",
    audience: "<идентификатор вашей интеграции>",
    clockTolerance: 60,
    maxTokenAge: 300 + 60,
    requiredClaims: ["sub", "iat", "exp", "jti"],
  });
  await rememberJtiOrReject(payload.jti, payload.exp + 60);
  return payload;
}
```

`remember_jti_or_reject` / `rememberJtiOrReject` — ваше хранилище принятых `jti` с атомарной записью: например, `SET widget-jti:<jti> 1 NX EXAT <until>` в Redis. Команда ничего не записала — токен уже предъявляли, отклоните его.

## Ротация ключа

Мы меняем ключ подписи на новый, с новым `kid`, по шагам:

**1. Новый ключ появляется в JWKS**

Заранее и вторым в списке — токены им ещё не подписываются.

**2. Новый ключ начинает подписывать**

Через несколько минут новые токены подписываются уже им. Старый ключ остаётся в JWKS.

**3. Старый ключ убирается из JWKS**

Не раньше чем через 300 секунд — срок жизни токена — плюс время кеша JWKS на вашей стороне.

> [!TIP]
>
> Держите в кеше все ключи из JWKS, а не один. Тогда во время ротации проходят токены и со старым, и с новым `kid`. Пришёл токен с незнакомым `kid` — перечитайте JWKS, но не чаще раза в минуту.

> [!WARNING]
>
> При компрометации ключ убирается из JWKS сразу, без шагов выше. Не кешируйте JWKS дольше 10 минут, не вшивайте ключи в код и конфиги.

## Требования к странице виджета

Виджет открывается во фрейме внутри кабинета Ai-Seven, а по кнопке «Открыть в новой вкладке» — в отдельной вкладке, со свежим токеном. Страница должна работать в обоих случаях, и так, чтобы токен не утёк:

> [accordion] Разрешите встраивание только нам
>
> Отдавайте страницу виджета с заголовком `Content-Security-Policy: frame-ancestors https://aisevenai.ru`. Заголовок `X-Frame-Options: DENY` или `SAMEORIGIN` встраивание запретит.

> [accordion] Ограничения фрейма: оплата — в новой вкладке, свои окна, файлы
>
> Фрейм открывается с атрибутом `sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"` — других разрешений нет.
>
> Навигация верхнего окна запрещена (нет `allow-top-navigation`): виджет не может увести вкладку Ai-Seven на свой адрес. Оплату, вход и любые свои окна открывайте новой вкладкой — ссылкой с `target="_blank"` или `window.open`.
>
> Диалоги браузера (`alert`, `confirm`, `prompt`) и скачивание файлов во фрейме не работают. Вместо диалогов показывайте свои окна на странице, файлы открывайте новой вкладкой.

> [accordion] Сессия во фрейме и сторонние cookie
>
> Во фрейме ваша страница — сторонний сайт, и браузеры режут её cookie: без подготовки обмен токена на сессию во фрейме может не сработать. Подойдёт один из способов:
>
> * **Отдельная cookie только для сессии виджета** — с `SameSite=None; Secure; Partitioned`. Основную сессию вашего сайта на `SameSite=None` не переводите. Каждый изменяющий запрос виджета защищайте CSRF-токеном или проверкой заголовка `Origin`.
> * **Сессия в памяти страницы** — после обмена токена храните её в переменной и передавайте в заголовке `Authorization`.
>
> Продолжение в новой вкладке, например оплату, передавайте одноразовым кодом: выдайте код примерно на 60 секунд, откройте вкладку с ним, и ваш сервер обменяет код на сессию. Сессию и `public_id` в адрес не кладите. При входе по коду тоже показывайте, под кем продолжается сессия, и просите подтверждение — как в разделе [Кто открыл ссылку](#кто-открыл-ссылку).

> [accordion] Referrer-Policy: no-referrer
>
> Отдавайте страницу виджета с заголовком `Referrer-Policy: no-referrer` — иначе адрес с токеном уйдёт в `Referer` запросов к сторонним ресурсам страницы.

> [accordion] Уберите токен из адреса
>
> Лучше всего — обмен на сервере с ответом `303` на тот же адрес без `token`. В одностраничном приложении — отправка токена на свой сервер в теле `POST` и `history.replaceState`, как в [проверке токена](#проверка-токена-на-вашей-стороне). Обновление страницы не должно требовать токена.

> [accordion] Не пускайте адрес с токеном в журналы и аналитику
>
> Не пишите строку запроса страницы виджета никуда, где её прочтут: в журналы веб-сервера, балансировщика, CDN и WAF, в трекеры ошибок (Sentry и подобные), в APM и системы аналитики. Если счётчик аналитики загружается на странице виджета, он должен стартовать после того, как токен убран из адреса.


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