# Подключение проекта к входу через КБП

Версия контракта: 1.0 · 19 октября 2026 года. Адрес этой инструкции: https://lk.kbp.kz/help/sso

Это публичная документация для разработчиков сторонних приложений: игр, сайтов и других самостоятельных сервисов. Она описывает порядок подключения и требования к интеграции. Документация не содержит действующих закрытых ключей, паролей, пользовательских данных или сведений о зарегистрированных проектах.

## 1. Что делает КБП, а что — приложение

КБП на https://lk.kbp.kz — единственный источник учётной записи и пароля. Используем OpenID Connect (OIDC), Authorization Code Flow с PKCE S256. Центральный компонент — `oidc-provider` 9.12.2; пример приложения — `openid-client` 6.8.8 и `jose` 6.2.10. Протокол самостоятельно не переписывайте: используйте поддерживаемую OIDC-библиотеку своего языка.

Игра получает подтверждение личности и создаёт собственный профиль, прогресс, права и сессию. Пароль вводится только на сайте КБП. Приложение не получает административные права КБП, рекламные балансы, документы, БИН, телефон или дату рождения. SSO-токен не подходит для вызовов обычного API кабинета или админки.

**Готовый центр и пример не означают, что ваша игра уже подключена.** Для рабочего подключения нужны её точные адреса, открытые ключи, настройка в реестре и проверенная реализация отзыва на её сервере.

## 2. Как получить подключение

1. Подготовьте серверное приложение на HTTPS. SPA или мобильный интерфейс использует собственный backend/BFF; закрытый ключ и токены не размещаются в JavaScript браузера, localStorage, мобильном бинарном файле или на Тильде.
2. Создайте отдельную RSA-пару 3072 бит (допустимы 2048–4096), алгоритм RS256, уникальный `kid`. В примере: `node keys.mjs`. Файл `private/client-key.json` остаётся на сервере приложения, режим 0600. Файл `private/session-key` также закрытый.
3. Передайте главному администратору КБП **только** `client-public-jwks.json`, название проекта, запрашиваемые scopes и точные URL из таблицы ниже. Открытый JWKS не секретен. Поля `d`, `p`, `q`, `dp`, `dq`, `qi` в передаваемом ключе недопустимы.
4. Главный администратор с действующей 2FA открывает https://admin.kbp.kz/sso-admin → «Единый вход → Проекты», создаёт подключение и сообщает `client_id`. `client_id` — публичный идентификатор, не пароль.
5. Настройте сервер приложения, проведите проверки из раздела 13. После проверки администратор включает проект.

Вместо общего `client_secret` используется **private_key_jwt**: сервер приложения подписывает короткое одноразовое утверждение своим закрытым ключом. КБП проверяет его по зарегистрированному открытому JWKS. При таком методе КБП не выдаёт и не хранит секрет приложения; пароль аккаунта КБП для интеграции не нужен. Изображения экрана с ключами и содержимое закрытых файлов никому не отправляйте.

| Настройка проекта | Пример, заменить своим точным адресом |
| --- | --- |
| Возврат после входа (redirect_uri) | `https://game.example.com/auth/kbp/callback` |
| После выхода (post_logout_redirect_uri) | `https://game.example.com/logged-out` |
| Серверный отзыв (backchannel_logout_uri) | `https://game.example.com/auth/kbp/backchannel-logout` |

Все URL регистрируются целиком. Разный путь, query, регистр пути, завершающий слеш — другой адрес. Wildcard, префиксы, фрагменты, внутренние IP, localhost и динамические перенаправления в production запрещены. Адреса выхода относятся к origin зарегистрированных адресов входа. КБП проверяет публичный DNS серверного адреса отзыва и закрепляет проверенный IP на время запроса.

## 3. Адреса центра

Issuer: **`https://lk.kbp.kz/api/sso/oidc`** — сравнивается точно, без завершающего `/`.

Discovery: **https://lk.kbp.kz/api/sso/oidc/.well-known/openid-configuration**

Загружайте metadata из Discovery библиотекой, а не собирайте конечные адреса догадками. Для этой версии:

| Назначение | Адрес | Вызов |
| --- | --- | --- |
| Начало входа | `https://lk.kbp.kz/api/sso/oidc/auth` | GET, браузер |
| Обмен кода / refresh | `https://lk.kbp.kz/api/sso/oidc/token` | POST form, сервер приложения |
| Открытые ключи центра | `https://lk.kbp.kz/api/sso/oidc/jwks` | GET |
| Данные личности | `https://lk.kbp.kz/api/sso/oidc/me` | GET, сервер с access token |
| Актуальность токена | `https://lk.kbp.kz/api/sso/oidc/token/introspection` | POST form, private_key_jwt |
| Отзыв токена | `https://lk.kbp.kz/api/sso/oidc/token/revocation` | POST form, private_key_jwt |
| Завершение текущей центральной сессии | `https://lk.kbp.kz/api/sso/oidc/session/end` | Браузерный OIDC RP-Initiated Logout |

Публичная самостоятельная регистрация OAuth-клиентов, implicit/password/client_credentials grant и plain PKCE не поддерживаются. API регистрации/редактирования проекта требует административной сессии КБП с 2FA; игровой токен туда не подходит.

## 4. Пошаговый вход

1. Кнопка «Войти через КБП» ведёт на **свой сервер** `/login`.
2. Сервер создаёт криптографически случайные `state`, `nonce`, `code_verifier` (не меньше 32 байт случайности). Генерирует S256 challenge. Хранит значения только на сервере в одноразовом контексте, привязанном к HttpOnly cookie браузера, максимум 15 минут.
3. Сервер перенаправляет браузер на authorization_endpoint с `client_id`, точным `redirect_uri`, `response_type=code`, `scope`, `state`, `nonce`, `code_challenge`, `code_challenge_method=S256`. Для `offline_access` обязательно `prompt=consent`. Для языка: `ui_locales=ru` или `kk`. Вход идёт в обычной вкладке, без iframe и сторонних cookie.
4. При действующей сессии КБП повторный пароль не требуется. Иначе пользователь входит обычным способом. Новый пользователь регистрируется и подтверждает email. После подтверждения в том же браузере может продолжить вход в проект; просроченный/открытый на другом устройстве контекст требует снова начать вход из приложения. Даже старый аккаунт без подтверждённого email не получает SSO-доступ до подтверждения.
5. КБП показывает название проекта и запрашиваемые данные. Пользователь продолжает или отказывается.
6. Callback приложения **однократно** забирает и удаляет свой серверный контекст, сравнивает state и обменивает код на сервере, передавая исходный verifier и private_key_jwt. При несовпадении, ошибке/отказе или отсутствии cookie сессию не создаёт.
7. OIDC-библиотека проверяет подпись ID Token по JWKS, `iss`, `aud` своего client_id, `exp`, `iat`, `nonce`, ожидаемый code flow и ответ. Включите проверку подписи ID Token и при прямом HTTPS-обмене; в примере это `enableNonRepudiationChecks`.
8. При необходимости сервер запрашивает UserInfo и проверяет, что его `sub` совпадает с ID Token. В примере, запрашивающем email, дополнительно проверяется `email_verified === true`.
9. В транзакции создаётся/находится собственный профиль по `(issuer,sub)` и создаётся новая случайная локальная сессия. Cookie: host-only, HttpOnly, Secure, SameSite=Lax, без `Domain=.kbp.kz`. Обычный callback не принимает пользовательский `returnTo` на произвольный сайт.
10. Сразу уберите код из адресной строки редиректом на свою обычную страницу. Не сохраняйте код, токен или полную строку callback в access/error/analytics-логах.

Минимальное начало входа на Node.js (полный работающий код — в архиве):

```js
import * as oidc from 'openid-client';
import { importJWK } from 'jose';
const key = await importJWK(privateJwk, 'RS256'); // закрытый файл на сервере
const config = await oidc.discovery(
  new URL('https://lk.kbp.kz/api/sso/oidc'), clientId,
  { token_endpoint_auth_method: 'private_key_jwt', id_token_signed_response_alg: 'RS256' },
  oidc.PrivateKeyJwt({ key, kid: privateJwk.kid }),
  { execute: [oidc.enableNonRepudiationChecks], timeout: 5 }
);
const verifier = oidc.randomPKCECodeVerifier();
const state = oidc.randomState(), nonce = oidc.randomNonce();
// Сохранить verifier/state/nonce на сервере и связать с одноразовой cookie.
const url = oidc.buildAuthorizationUrl(config, {
  redirect_uri: registeredCallback,
  response_type: 'code', scope: 'openid profile email offline_access', prompt: 'consent',
  state, nonce, code_challenge: await oidc.calculatePKCECodeChallenge(verifier),
  code_challenge_method: 'S256', ui_locales: 'ru'
});
// Ответ своего сервера: redirect 303 на url.href.
```

Обработка ответа библиотекой:

```js
const tokens = await oidc.authorizationCodeGrant(config, callbackUrl, {
  pkceCodeVerifier: saved.verifier, expectedState: saved.state,
  expectedNonce: saved.nonce, idTokenExpected: true
});
const identity = tokens.claims();
const info = await oidc.fetchUserInfo(config, tokens.access_token, identity.sub);
// Проверки должны закончиться ДО INSERT профиля и сессии.
```

Не заменяйте проверку подписи простым `JSON.parse` JWT. ID Token — подтверждение входа, а не способ бесконечно считать аккаунт активным.

## 5. Данные и scopes

| Scope | Данные / действие |
| --- | --- |
| `openid` (обязателен) | `sub`: постоянный непрозрачный идентификатор пользователя |
| `profile` | `name`: имя из профиля КБП |
| `email` | `email`, `email_verified: true` |
| `offline_access` | Разрешает серверное обновление через refresh token |

Клиент может запросить только scopes, разрешённые в его карточке. Лишний scope отклоняется. Технические поля OIDC (`iss`, `aud`, `iat`, `exp`, `nonce`, `sid`) обеспечивают проверку, не являются пользовательскими правами. `sid` связывает центральную сессию с уведомлением о выходе; сохраните его в локальной сессии.

Для минимальной идентификации достаточно `openid`; имя и email запрашивайте по необходимости. Предлагаемый пример использует все четыре scopes, поэтому администратор должен разрешить их именно этому примеру. В локальном приложении `email` и `name` — обновляемые атрибуты, а не ключ личности. Нельзя автоматически объединять разные `(issuer,sub)` по совпавшей почте.

```sql
CREATE UNIQUE INDEX game_profiles_identity ON game_profiles(issuer,subject);
-- Атомарный INSERT ... ON CONFLICT(issuer,subject) ... RETURNING id.
```

Смена email не создаёт второго профиля и не переносит прогресс. `sub` не меняется. КБП не принимает игровые пароли и не управляет игровой базой.

## 6. Сроки и хранение

| Объект | Максимальный срок |
| --- | --- |
| Контекст входа / подтверждение перехода | 15 минут, связан с браузером |
| Authorization code | 60 секунд, однократно |
| Access Token | 5 минут |
| ID Token | 5 минут |
| Refresh Token / разрешение проекта | 30 дней от первоначальной выдачи, без бесконечного продления |
| Центральная OIDC-сессия | 14 дней |
| Локальная сессия примера | 8 часов, собственная cookie |
| Кэш успешной introspection в приложении | Не более 30 секунд от начала проверки |
| Предельная потеря доступа при отзыве | 60 секунд для корректно подключённого проекта |

Обычная сессия кабинета КБП остаётся прежней: это отдельная cookie и прежние правила входа, 2FA и выхода. Истечение локальной сессии требует нового входа; refresh не должен восстанавливать уже отозванную локальную сессию.

На сервере приложения храните токены зашифрованными, отдельным ключом вне базы/репозитория. В примере AES-256-GCM с отдельным случайным nonce на запись и проверкой AAD. Cookie содержит только случайный идентификатор; в БД — его hash. Не помещайте токены в URL, HTML, письма, Telegram, логи, аналитику или localStorage.

## 7. Проверка актуальности — обязательная часть

Подписи ID Token недостаточно для выполнения игровых действий. Перед **каждым защищённым запросом** приложение проверяет собственную неотозванную сессию и результат introspection. Успешный ответ можно кэшировать максимум 30 секунд, считая время с начала запроса. Проверяйте `active === true`, `client_id === ваш client_id`, `sub === sub сессии`.

```js
const status = await oidc.tokenIntrospection(config, accessToken);
if (status.active !== true || status.client_id !== clientId || status.sub !== session.subject) {
  // Отозвать локальную сессию, запретить защищённое действие, предложить новый вход.
}
```

При блокировке аккаунта, удалении, сбросе/смене пароля, потере подтверждения email, общем выходе или отключении проекта центральная проверка сразу перестаёт подтверждать старый доступ. Разблокировка не оживляет старые токены и сессии.

Если КБП недоступен, сетевой таймаут или ответ не проверен, после окончания допустимого кэша действие **запрещается**. Нельзя продлевать кэш при ошибке или продолжать игру по одному ID Token. В примере сессия при такой ошибке завершается; проект может вместо этого временно закрыть защищённые действия, сохранив данные, но не разрешать их без актуальной проверки.

Именно приложение обеспечивает этот предел для своих API, WebSocket-сообщений и фоновых пользовательских действий. Для долгого WebSocket проверяйте доступ периодически не реже 30 секунд и перед изменением защищённых данных. Центр не может закрыть произвольную сессию игры, которая игнорирует отзыв.

## 8. Серверный выход Back-Channel Logout

Зарегистрированный HTTPS endpoint приложения принимает POST `application/x-www-form-urlencoded`, поле `logout_token`. Cookie браузера и Origin здесь не нужны: это подписанное серверное уведомление. Исключение Origin ограничьте **только этим endpoint**, не всем API.

Используйте JWT-библиотеку. Проверяются RS256, подпись по JWKS КБП, точный issuer, audience своего клиента, обязательные `iat`, `jti`, `events`, событие `http://schemas.openid.net/event/backchannel-logout`, отсутствие `nonce`, наличие `sid` и/или `sub`. Пример принимает токен возрастом до 120 секунд с допуском часов 5 секунд. Часы серверов должны быть синхронизированы.

В одной транзакции зарегистрируйте уникальный `(issuer,jti)` и отзовите совпадающие **локальные** сессии по issuer + sid, при наличии sub также проверяя его. Если sid отсутствует, завершите локальные сессии этого sub. Повторная доставка того же токена должна вернуть успешный 200/204 и не удалять профиль/прогресс. Недействительная подпись или чужой audience — 400/401 без изменений.

КБП хранит очередь и историю доставки. Первая попытка обычно в течение 5 секунд; сетевой таймаут 5 секунд; до 12 попыток с интервалами 5,10,20,40,80,160, затем 300 секунд. Каждая попытка получает свежий подписанный токен. История доступна главному администратору в карточке проекта; завершённые записи технической доставки хранятся 90 дней, общий безопасный аудит — по политике КБП. Очередь не заменяет introspection: при потере всех уведомлений предел отзыва сохраняется через проверку актуальности.

## 9. Три разных действия выхода

- **«Выйти» в игре**: завершить только текущую локальную игровую сессию (POST с Origin + CSRF), очистить свою cookie. Другие проекты и кабинет продолжают работать. Не вызывайте центральный общий выход и не отзывайте общий grant при каждом локальном выходе.
- **RP-Initiated Logout**: стандартный `end_session_endpoint` из Discovery с проверенным `id_token_hint`, зарегистрированным `post_logout_redirect_uri` и state, который проверяется приложением при возврате. КБП запрашивает подтверждение. Завершается текущая центральная OIDC-сессия и связанные разрешения; это не команда «все устройства». Обычная cookie кабинета не переопределяется этим протоколом.
- **«Выйти из всех сервисов на всех устройствах» в профиле КБП**: отдельная явно подтверждаемая команда; отзывает все сессии кабинета и центральные разрешения этого пользователя на всех устройствах, ставит уведомления проектам. Игра должна обработать их и/или закрыть доступ по introspection. Данные сохраняются.

Обычная кнопка выхода из кабинета КБП сохраняет прежний смысл и не превращается в незаметный общий выход.

## 10. Refresh, отзыв и ошибки

`offline_access` нужен только если серверу действительно требуется refresh. Access token можно обновить через `oidc.refreshTokenGrant(config, refreshToken)`. Refresh token меняется при каждом обмене; сохраняйте новую пару атомарно, сериализуйте обновление одной локальной сессии. Пример удерживает блокировку строки локальной сессии на время ограниченного 5-секундным таймаутом обмена; используйте отдельную БД приложения и разумные таймауты блокировок.

Повтор старого refresh token отзывает **всю семью разрешения**, включая выданные при ротации токены. `invalid_grant` → закрыть локальную сессию и начать новый вход. Не повторяйте бесконечно обмен старого кода/refresh. 30 дней отсчитываются от первоначальной выдачи, а не от каждого refresh.

Для отдельного явного отключения доступа приложения используйте token_revocation_endpoint библиотекой (`oidc.tokenRevocation`), затем завершите локальную сессию. Это отличается от обычного локального выхода.

| Ситуация | Действие приложения |
| --- | --- |
| `access_denied` | Показать отказ и кнопку повторного входа, не создавать профиль/сессию |
| Нет контекста / неверные state или nonce | Отклонить callback; новый вход из приложения |
| `invalid_grant` | Код/refresh больше не использовать, войти заново |
| `invalid_client` | Закрыть операцию, проверить client_id/JWKS/kid/подпись/часы на сервере |
| `invalid_scope` | Исправить запрос под разрешённые scopes |
| Introspection `active:false` | Отозвать локальную сессию и запретить действие |
| Недоступность/429/5xx | Без обхода проверки, без продления кэша, без вывода токенов в ошибку |

## 11. Смена ключей

**Ключи приложения:** создаёт и хранит приложение. Новый открытый JWKS администратор сохраняет в карточке проекта. Изменение подключения (в том числе ключей/адресов/scopes) отзывает старый доступ только этого проекта; пользователи входят заново. Закрытый ключ не передаётся. Отключение проекта не блокирует пользователя и другие приложения.

**Ключи подписи центра:** `kid` может меняться. Используйте Discovery/JWKS и стандартное обновление ключей при неизвестном kid. Обычная ротация двухэтапная: новый ключ сначала появляется в JWKS, прежний продолжает подписывать; после минимум 6 минут (окно обновления кэша JWKS до 5 минут) новый становится активным. Приложение должно обновлять кэш JWKS не реже 5 минут. Центр сохраняет прежний открытый ключ минимум 24 часа; закрытый старый ключ не используется для новых токенов. Действующие токены 5 минут продолжают проверяться, сами refresh-семьи не продлеваются. Не закрепляйте единственный ключ навсегда и не отключайте проверку подписи при смене kid.

При компрометации ключа действует аварийная процедура: отзыв затронутого доступа и удаление скомпрометированного ключа, без обещания совместимости старых токенов. Это отдельная административная операция, не автоматическое действие подключённой игры. Ключ шифрования хранилища и cookie-подписи не заменяйте файлом другой среды.

## 12. Запуск рабочего примера

Архив: https://lk.kbp.kz/docs/kbp-sso-client.zip
Markdown этой инструкции: https://lk.kbp.kz/docs/kbp-sso-integration.md

Состав: `app.mjs`, `server.mjs`, `keys.mjs`, `schema.sql`, `.env.example`, `package.json`, lockfile и README. Пример не содержит игры, платежей и готовых действующих ключей. Это серверное приложение с отдельными PostgreSQL-профилями и сессиями.

1. Node.js 22.23+; PostgreSQL для **собственной БД приложения**. Установите зависимости `pnpm install --frozen-lockfile` (либо ваш проверенный менеджер).
2. Выполните `schema.sql` в своей БД, не в базе аккаунтов КБП.
3. `node keys.mjs`; настройте закрытый `.env` по `.env.example`; выдайте секретным файлам 0600, каталогу private — 0700.
4. Передайте открытый JWKS и точные URL администратору, получите свой client_id и разрешённые scopes.
5. Запустите `pnpm start` за HTTPS reverse proxy своего домена. Не публикуйте private/.env/логи callback. Cookie принадлежат только своему host.
6. Откройте `/login`, пройдите вход; `/me` показывает отдельный профиль. Локальный выход не завершает другие сервисы.
7. Проверьте блокировку/потерю уведомления/отзыв/дубли на искусственном тестовом аккаунте в согласованной среде. В production не создавайте фиктивные игровые платежи или массовые аккаунты.

Локальный HTTP разрешён **только** при явном `SSO_LOCAL_TEST=true`, `NODE_ENV != production`, обоих origin на `http://127.0.0.1:<port>` и отдельном локальном центре с отдельными ключами/БД. Рабочий центр КБП не регистрирует localhost. Не включайте insecure-режим для production-issuer или реальных аккаунтов.

Пример — основа интеграции, не готовая игровая платформа: добавьте свои игровые правила, права, API, ограничения частоты, обработку инфраструктурных ошибок, резервирование и очистку истёкших локальных контекстов/сессий/jti. Не удаляйте профили и прогресс при обычном выходе или блокировке.

## 13. Приёмка приложения разработчиком

Перед подключением должны проходить: проверенный вход; повторный вход без второго профиля; регистрация/email и восстановление с возвратом; отказ неподтверждённому/блокированному аккаунту; точные redirect; неверные state/nonce/PKCE; истёкший и повторный код; чужой client_id/audience; смена email без потери профиля; refresh rotation/replay; подпись и дубли logout; блокировка открытого приложения ≤60 секунд; потеря back-channel с тем же пределом; недоступность центра после 30-секундного кэша; локальный выход без выхода других проектов; общий выход без удаления прогресса; отключение одного клиента; ротация JWKS; мобильный интерфейс и язык.

Проверка центра и эталонного клиента не заменяет приёмку будущей игры. Разработчик игры отвечает за охват всех защищённых действий актуальной проверкой, собственные роли, профиль, прогресс, покупки, резервные копии, логи и безопасность своего сервера.

## 14. Источники стандартов

- OIDC Core: https://openid.net/specs/openid-connect-core-1_0.html
- Discovery: https://openid.net/specs/openid-connect-discovery-1_0.html
- OAuth Security BCP: https://www.rfc-editor.org/rfc/rfc9700.html
- Introspection: https://www.rfc-editor.org/rfc/rfc7662.html
- Revocation: https://www.rfc-editor.org/rfc/rfc7009.html
- Back-Channel Logout: https://openid.net/specs/openid-connect-backchannel-1_0.html
- RP-Initiated Logout: https://openid.net/specs/openid-connect-rpinitiated-1_0.html
- Библиотеки: https://github.com/panva/node-oidc-provider и https://github.com/panva/openid-client
