Перейти к основному содержимому

Публичный API, OAuth 2.0 и webhooks

Разработчику Архитектору Аналитику

Зачем отдельная глава

В первом сквозном примере (раздел «Сквозной пример — партнёрский API заказов») разобран B2B сценарий: сервер-сервер, Bearer от вашего IdP, курсор, Idempotency-Key. Здесь — типичный публичный API: сторонние приложения от имени пользователя, ограниченные scopes, и webhooks (ваш сервер принимает HTTP POST от партнёра или от вашей же очереди доставки событий).

База по терминам: 2.09. API, HTTP. Полный список соседних статей — в конце той же главы 117 (блок «Маршрут по материалам API»).


Контекст — «TaskBoard» для интеграторов

Продукт: облачная доска задач с публичным REST API.

Потребители:

  • мобильное и десктопное приложение (от имени человека);
  • CI-плагин (только машина, без UI);
  • маркетплейс, которому нужны события (task.created, task.completed), чтобы не опрашивать GET каждые N секунд.

Цели API:

  • не хранить пароль приложения у стороннего разработчика;
  • явно ограничить доступ (read vs write);
  • принимать webhooks быстро и без дублей при повторной доставке.

OAuth 2.0 — какие потоки выбрать

СценарийПоток OAuth 2.0Кратко
Приложение с UI «войти через TaskBoard»Authorization Code + PKCEбраузер открывает authorize, callback с code, обмен на токены
Скрипт, сервисный аккаунт, nightly jobClient Credentialsclient_id + client_secret (или assertion), только machine-to-machine
Устаревшее встроенное приложение без браузераDevice Authorizationреже; отдельный UX с кодом на экране

PKCE обязателен для публичных клиентов (SPA, мобильное), где нельзя хранить client_secret. Секрет остаётся на сервере бэкенда приложения, если есть свой backend-for-frontend — там допустим классический code exchange с client_secret на сервере.


Шаг 1 — регистрация приложения

Разработчик создаёт OAuth client в портале разработчика:

  • redirect URIs — белый список https://myapp.example/oauth/callback (без wildcard в production);
  • grant typesauthorization_code, refresh_token; для CI — отдельный client только с client_credentials;
  • scopes — строки вроде tasks:read, tasks:write, webhooks:manage.

Шаг 2 — Authorization Code + PKCE (упрощённая последовательность)

  1. Клиент генерирует code_verifier (случайная строка 43–128 символов), считает code_challenge = BASE64URL(SHA256(verifier)).
  2. Браузер открывает:
GET /oauth/authorize?response_type=code
&client_id=app_abc123
&redirect_uri=https%3A%2F%2Fmyapp.example%2Foauth%2Fcallback
&scope=tasks%3Aread%20tasks%3Awrite
&state=random_csrf_token
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
  1. После согласия пользователя браузер перенаправляет на redirect_uri?code=...&state=....
  2. Бэкенд приложения обменивает code на токены:
POST /oauth/token HTTP/1.1
Host: auth.taskboard.example
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https://myapp.example/oauth/callback
&client_id=app_abc123
&client_secret=... # только для confidential client
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
  1. Ответ:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200...",
"scope": "tasks:read tasks:write"
}

Access token несёт scopes и субъект (user id или client id). Refresh token хранят защищённо, меняют по ротации (one-time refresh) если политика безопасности требует.


Шаг 3 — Client Credentials

POST /oauth/token HTTP/1.1
Host: auth.taskboard.example
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=ci_plugin_xyz
&client_secret=...
&scope=tasks:read

Ответ — только access_token без refresh; права жёстко ограничены отдельным client, не пользовательскими ролями.


Фрагмент OpenAPI — security

components:
securitySchemes:
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.taskboard.example/oauth/authorize
tokenUrl: https://auth.taskboard.example/oauth/token
scopes:
tasks:read: Чтение задач
tasks:write: Создание и изменение
webhooks:manage: Подписка на webhooks
clientCredentials:
tokenUrl: https://auth.taskboard.example/oauth/token
scopes:
tasks:read: Чтение задач
security:
- OAuth2: [tasks:read, tasks:write]

Документация контракта — 7.08. OpenAPI.


Вызов API с токеном

GET /v1/tasks?projectId=prj_01 HTTP/1.1
Host: api.taskboard.example
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

Сервер API не ходит в IdP на каждый запрос, если использует локальную проверку JWT (JWKS) или introspection для opaque токенов — решение фиксируют в NFR и threat model (8.07.128).


Webhooks — исходящие события к подписчику

Модель: при изменении задачи платформа ставит событие в очередь; delivery worker делает POST на URL, который подписчик указал при создании подписки.


Регистрация подписки (REST)

POST /v1/webhooks HTTP/1.1
Host: api.taskboard.example
Authorization: Bearer …
Content-Type: application/json

{
"url": "https://partner.example/hooks/taskboard",
"events": ["task.created", "task.completed"],
"secret": "whsec_..."
}

secret генерирует сервер и показывает один раз — по нему подписчик проверяет подлинность тел (аналог Stripe signing secret).


Тело доставки

Единый конверт:

{
"id": "evt_01JABC123",
"type": "task.completed",
"createdAt": "2026-05-27T12:00:05Z",
"data": {
"taskId": "tsk_99",
"projectId": "prj_01",
"completedBy": "usr_7"
}
}

Идемпотентность: evt_01JABC123 уникален; подписчик хранит обработанные id (Redis или таблица) минимум 24–72 часа и отвечает 200 с тем же телом при повторе — см. идемпотентность.


Заголовки доверия

ЗаголовокНазначение
X-Webhook-Idдубликат id из тела, удобно для логов без парсинга JSON
X-Webhook-TimestampUnix секунды момента подписи
X-Webhook-Signatureнапример v1=base64(hmac_sha256(secret, timestamp + '.' + raw_body))

Подписчик:

  1. Отклоняет запросы старше 5 минут по X-Webhook-Timestamp (защита от replay).
  2. Считает HMAC от сырых байт тела до JSON-парсинга (иначе меняется канонизация).
  3. Сравнивает константным временем (crypto.timingSafeEqual в Node, аналоги в других языках).

Пример проверки (Node.js, иллюстрация):


import crypto from 'crypto';

function verifyWebhook(rawBody, timestamp, sigHeader, secret) {
const ageSec = Math.abs(Date.now() / 1000 - Number(timestamp));
if (ageSec > 300) return false;
const payload = `${timestamp}.${rawBody}`;
const expected = 'v1=' + crypto.createHmac('sha256', secret).update(payload).digest('base64');
const got = sigHeader.trim();
const a = Buffer.from(expected);
const b = Buffer.from(got);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Ответ подписчика и повторы доставки

  • Ответить 2xx в течение 5–15 с` — обработку кладут во внутреннюю очередь, не в request handler.
  • 429 или 5xx — платформа планирует retry с экспоненциальной задержкой и jitter, максимум N попыток, затем dead letter и алерт владельцу подписки.
  • 401 из-за неверной подписи — после нескольких попыток подписку паузят и пишут владельцу (часто это сломанный secret).

Исходящая сторона (ваш delivery) описана в общих чертах в 117 (подзаголовок про polling, webhooks и streaming).


Мини-чеклист перед публикацией публичного API

  1. Redirect URI — только HTTPS, список в консоли, отдельные clients для prod и sandbox.
  2. Scopes — минимальный набор по умолчанию; опасные операции вынести в отдельный scope.
  3. Rate limit — по client_id и по пользователю; заголовки Retry-After (8.05.122).
  4. Webhooks — подпись, timestamp, идемпотентность по evt id, быстрый ACK.
  5. Версия — префикс /v1/, политика sunset в документации.
  6. Тесты7.05.2, контракт по OpenAPI.

Смежные материалы

ТемаСтатья
Первый сквозной пример (B2B, Idempotency-Key)7.06.117
Аутентификация в интеграциях7.06.117
Идемпотентность7.06.213
Уязвимости API8.07.128
Маршрут по всем главам7.06.117
mTLS, JWS, AsyncAPI, outbox7.06.1172

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").