SSO / token-exchange
Обмен внешнего идентификатора пользователя на партнёрский JWT по RFC 8693. Встройте кабинет инфлюенсера MeerPartners в свой продукт без повторной регистрации. Поля, ответ и пример.
Token-exchange позволяет авторизовать ваших пользователей внутри партнёрки, не заставляя их регистрироваться в MeerPartners и не проводя через MeerID. Ваш бэкенд обменивает свой идентификатор пользователя (например telegram user id или внутренний UUID) на короткоживущий партнёрский JWT со скоупом affiliate:embed. С этим токеном можно открыть виджеты партнёрки прямо в вашем интерфейсе.
Механика следует RFC 8693 (OAuth 2.0 Token Exchange).
/api/v1/auth/token-exchange🔒 HMAC (scope sso)Запрос подписывается HMAC по секрету API-ключа со скоупом sso — той же схемой, что и postback. Заголовки и алгоритм: API-ключи и подпись.
Когда это нужно
- У вас есть свой продукт с авторизованными пользователями (сайт, приложение, Telegram Mini App).
- Вы хотите дать им кабинет инфлюенсера (ссылки, статистика, выплаты) внутри продукта.
- Вы не хотите вторую регистрацию и не хотите гонять их через экран входа MeerID.
Если же нужно просто отправлять конверсии — token-exchange не требуется, достаточно postback.
Как это работает
Ваш фронтенд Ваш бэкенд MeerPartners
│ │ │
│ открыть «Партнёрку» │ │
│ ────────────────────► │ │
│ │ POST /api/v1/auth/token-exchange
│ │ (HMAC по ключу scope sso) │
│ │ ────────────────────────►│
│ │ │ резолв ключа → tenant
│ │ │ find-or-create партнёра
│ │ │ выпуск scoped JWT
│ │ ◄──── access + refresh ──│
│ ◄── scoped-токен ─────│ │
│ открыть виджеты с этим токеном │Tenant определяется только по API-ключу в базе — поле tenant из тела игнорируется (защита от подмены). Если партнёр под вашим subject_token ещё не существует, платформа создаёт его на лету (JIT) под защитой от гонок.
Поля запроса
Тело — JSON.
grant_typestringoptionalТип гранта. Значение по умолчанию и ожидаемое — urn:ietf:params:oauth:grant-type:token-exchange.
subject_tokenstringrequiredВаш идентификатор пользователя (1–255 символов). Платформа не интерпретирует его — хранит как opaque-строку для связывания вашего пользователя с партнёрским профилем.
subject_token_typestringoptionalТип субъекта. По умолчанию external_user_id.
subject_profileobjectoptionalНеобязательный профиль, применяется только при первом создании партнёра (JIT). Распознаются ключи: email, display_name, username (если нет display_name, берётся username). Это недоверенные данные — на существующего пользователя они не влияют.
tenant в теле игнорируется
Не пытайтесь передать tenant в теле запроса — он всё равно берётся из API-ключа (анти-IDOR). Один ключ работает строго со своим бизнесом.
Ответ
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "bearer",
"expires_in": 600,
"scope": "affiliate:embed",
"affiliate_id": 8123,
"tenant_id": 17
}| Поле | Значение |
|---|---|
access_token | Scoped JWT, scope affiliate:embed, TTL 10 минут |
refresh_token | Стандартный refresh (TTL 30 дней) — продлевает сессию без повторного обмена |
expires_in | TTL access-токена в секундах (600) |
scope | Всегда affiliate:embed |
affiliate_id | ID партнёрского профиля — чтобы SDK не делал /me сразу |
tenant_id | ID вашего бизнеса (из ключа) |
Scoped-токен ≠ полный доступ
Токен affiliate:embed урезан: он годится для виджетов кабинета инфлюенсера, но не даёт «опасных» действий. Например, «выйти из всех сессий» (/api/v1/auth/logout-all) и онбординг ролей требуют полного токена и для embed-токена вернут EMBED_SCOPE_DENIED (403).
Коды отклонения
| Код | HTTP | Когда |
|---|---|---|
TENANT_API_KEY_INVALID | 401 | Ключ неизвестен, неактивен или истёк; нет заголовка X-Api-Key-Id |
TENANT_API_KEY_SCOPE_DENIED | 403 | У ключа нет скоупа sso |
EXCHANGE_DENIED | 401 | Подпись невалидна, нет/просрочен X-Timestamp, tenant неактивен |
EXCHANGE_REPLAY | 401 | Повтор той же подписи в окне памяти |
USER_BANNED | 403 | Найденный пользователь заблокирован |
SERVICE_UNAVAILABLE | 503 | Хранилище replay-защиты недоступно (fail-closed) — повторите позже |
Свежесть X-Timestamp — те же ±300 с, что у postback. Формат конверта ошибки — Коды ошибок API.
Пример
Обмен внутреннего user-id на scoped-токен. Подпись считается по сырому телу запроса.
API_BASE="https://api.meerpartners.com"
KEY_ID="key_sso_77"
SECRET="sk_live_…"
BODY='{"grant_type":"urn:ietf:params:oauth:grant-type:token-exchange","subject_token":"vpn-user-99281","subject_token_type":"external_user_id","subject_profile":{"email":"u@vpn.io","display_name":"Neo"}}'
TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')
SIGNING=$(printf '%s\n%s\n%s\n%s' "POST" "/api/v1/auth/token-exchange" "$TS" "$BODY_HASH")
SIG=$(printf '%s' "$SIGNING" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
curl -sS -X POST "$API_BASE/api/v1/auth/token-exchange" \
-H "Content-Type: application/json" \
-H "X-Api-Key-Id: $KEY_ID" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
--data "$BODY"