> ## 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 по кнопке из вашего кабинета

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

Пользователь нажимает кнопку в вашем кабинете и попадает в Ai-Seven уже авторизованным — форму входа он не видит.

Личность пользователя подтверждает подписанный вами токен. Мы проверяем подпись вашим публичным ключом, находим или создаём аккаунт и выдаём пользователю обычную сессию Ai-Seven. Дальше он работает как любой другой пользователь платформы — выданный вами токен в его работе больше не участвует.

<Steps>
  <Step title="Пользователь нажимает кнопку">
    В вашем кабинете — «Открыть Ai-Seven» или любая другая формулировка.
  </Step>

  <Step title="Вы подписываете токен">
    Своим приватным ключом, алгоритмом RS256. Токен короткоживущий и одноразовый.
  </Step>

  <Step title="Перенаправляете пользователя">
    На `https://aisevenai.ru/auth/sso?token=<JWT>`
  </Step>

  <Step title="Мы авторизуем пользователя">
    Проверяем подпись, находим или создаём аккаунт, выдаём сессию и открываем кабинет.
  </Step>
</Steps>

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

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

```json theme={null}
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "<ваш kid>"
}
```

<ParamField header="kid" type="string" required>
  Идентификатор ключа, который мы выдали вам при подключении. По нему мы определяем, каким публичным ключом проверять подпись.
</ParamField>

<ParamField header="alg" type="string" required>
  Всегда `RS256`. Другие алгоритмы не принимаются.
</ParamField>

### Payload

```json theme={null}
{
  "aud": "ai-seven:sso",
  "sub": "u_12345",
  "user_info": {
      "first_name": "Иван",
      "last_name": "Петров"
  },
  "exp": 1754700120,
  "jti": "550e8400-e29b-41d4-a716-446655440000"
}
```

<ParamField body="aud" type="string" required>
  Всегда `ai-seven:sso`. Отличает токен входа от токена Partner API — они не взаимозаменяемы.
</ParamField>

<ParamField body="sub" type="string" required>
  Идентификатор пользователя в вашей системе. По нему пользователь опознаётся при каждом входе.
</ParamField>

<ParamField body="exp" type="integer" required>
  Момент истечения токена, unix-время. **Максимум** 120 секунд от момента подписи.
</ParamField>

<ParamField body="jti" type="string" required>
  Уникальный идентификатор токена, обычно UUID. Обеспечивает одноразовость: повторный вход по тому же токену невозможен.
</ParamField>

<ParamField body="user_info" type="object">
  Данные пользователя для отображения в кабинете. Объект и все его поля необязательны — передавайте то, что есть.

  <Expandable title="поля">
    <ParamField body="first_name" type="string">
      Имя.
    </ParamField>

    <ParamField body="last_name" type="string">
      Фамилия.
    </ParamField>
  </Expandable>
</ParamField>

<Warning>
  `sub` **должен быть стабильным.** Если идентификатор пользователя изменится, при следующем входе мы посчитаем его новым пользователем и создадим отдельный аккаунт.
</Warning>

## Пример: генерация токена

<CodeGroup>
  ```python Python theme={null}
  import uuid
  import time
  import jwt  # pip install pyjwt[crypto]

  PRIVATE_KEY = open("private.pem").read()
  KID = "123"


  def build_sso_url(user_id: str, user_info: dict | None = None) -> str:
      payload = {
          "aud": "ai-seven:sso",
          "sub": user_id,
          "exp": int(time.time()) + 120,
          "jti": str(uuid.uuid4()),
      }
      if user_info:
          payload["user_info"] = user_info

      token = jwt.encode(
          payload,
          PRIVATE_KEY,
          algorithm="RS256",
          headers={"kid": KID},
      )
      return f"https://aisevenai.ru/auth/sso?token={token}"


  url = build_sso_url(
      "u_12345",
      {"first_name": "Иван", "last_name": "Петров"},
  )
  ```

  ```javascript Node.js theme={null}
  import jwt from "jsonwebtoken";
  import { randomUUID } from "crypto";
  import { readFileSync } from "fs";

  const PRIVATE_KEY = readFileSync("private.pem");
  const KID = "123";

  function buildSsoUrl(userId, userInfo) {
    const payload = {
      aud: "ai-seven:sso",
      sub: userId,
      jti: randomUUID(),
    };
    if (userInfo) payload.user_info = userInfo;

    const token = jwt.sign(payload, PRIVATE_KEY, {
      algorithm: "RS256",
      expiresIn: 120,
      keyid: KID,
    });

    return `https://aisevenai.ru/auth/sso?token=${token}`;
  }

  const url = buildSsoUrl("u_12345", {
    first_name: "Иван",
    last_name: "Петров"
  });
  ```

  ```php PHP theme={null}
  <?php
  use Firebase\JWT\JWT;

  $privateKey = file_get_contents('private.pem');
  $kid = '123';

  function buildSsoUrl(string $userId, ?array $userInfo = null): string {
      global $privateKey, $kid;

      $payload = [
          'aud' => 'ai-seven:sso',
          'sub' => $userId,
          'exp' => time() + 120,
          'jti' => bin2hex(random_bytes(16)),
      ];
      if ($userInfo !== null) {
          $payload['user_info'] = $userInfo;
      }

      $token = JWT::encode($payload, $privateKey, 'RS256', $kid);
      return 'https://aisevenai.ru/auth/sso?token=' . $token;
  }

  $url = buildSsoUrl('u_12345', [
      'first_name' => 'Иван',
      'last_name' => 'Петров'
  ]);
  ```
</CodeGroup>

<Note>
  Токен генерируется на вашем бэкенде в момент перехода — не заранее и не на клиенте. Приватный ключ не должен попадать в браузер.
</Note>

## Аккаунты, созданные через вашу платформу

При первом входе пользователя мы автоматически создаём для него аккаунт в Ai-Seven.

<CardGroup cols={3}>
  <Card title="Без email" icon="mail">
    Email не передаётся и не запрашивается — для входа он не нужен.
  </Card>

  <Card title="Вход только через вас" icon="key-round">
    Привязка почты или Telegram для прямого входа в Ai-Seven недоступна.
  </Card>

  <Card title="Полный доступ к платформе" icon="award">
    Внутри Ai-Seven пользователь работает как любой другой.
  </Card>
</CardGroup>

## Ошибки

| Код   | Сообщение                                      | Причина                                                            |
| ----- | ---------------------------------------------- | ------------------------------------------------------------------ |
| `400` | Невалидный токен                               | Токен не разбирается как JWT — повреждён или обрезан               |
| `400` | Не указан kid                                  | В заголовке токена отсутствует `kid`                               |
| `401` | Ключ не найден или деактивирован               | Неизвестный `kid`, либо ключ или интеграция отключены              |
| `401` | Истёк срок действия токена                     | `exp` уже наступил                                                 |
| `401` | Невалидный токен                               | Неверная подпись, неверный `aud` или отсутствует обязательное поле |
| `401` | Срок жизни токена не соответствует требованиям | Токен выписан на слишком долгий срок                               |
| `401` | Токен уже использован                          | Токен с таким `jti` уже применялся                                 |

<Accordion title="Пользователь не может войти — что проверить">
  **Истёк срок действия токена.** Проверьте синхронизацию времени на своих серверах (NTP). Мы допускаем небольшое расхождение часов, но заметный сдвиг приводит к отклонению всех токенов.

  **Токен уже использован.** Каждый токен срабатывает один раз. Если пользователь обновил страницу или вернулся назад в браузере, нужен новый переход по кнопке — генерируйте токен на каждый переход заново.

  **Невалидный токен.** Убедитесь, что подписываете приватным ключом из той же пары, что и переданный нам публичный, алгоритмом `RS256`, и что в payload присутствуют все обязательные поля: `aud`, `sub`, `exp`, `jti`.

  **Ключ не найден.** Используйте `kid`, который мы выдали при подключении. Если интеграция была приостановлена, свяжитесь с нами.
</Accordion>
