Публичный 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 job | Client Credentials | client_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 types —
authorization_code,refresh_token; для CI — отдельный client только сclient_credentials; - scopes — строки вроде
tasks:read,tasks:write,webhooks:manage.
Шаг 2 — Authorization Code + PKCE (упрощённая последовательность)
- Клиент генерирует
code_verifier(случайная строка 43–128 символов), считаетcode_challenge = BASE64URL(SHA256(verifier)). - Браузер открывает:
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
- После согласия пользователя браузер перенаправляет на
redirect_uri?code=...&state=.... - Бэкенд приложения обменивает
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
- Ответ:
{
"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-Timestamp | Unix секунды момента подписи |
X-Webhook-Signature | например v1=base64(hmac_sha256(secret, timestamp + '.' + raw_body)) |
Подписчик:
- Отклоняет запросы старше 5 минут по
X-Webhook-Timestamp(защита от replay). - Считает HMAC от сырых байт тела до JSON-парсинга (иначе меняется канонизация).
- Сравнивает константным временем (
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
- Redirect URI — только HTTPS, список в консоли, отдельные clients для prod и sandbox.
- Scopes — минимальный набор по умолчанию; опасные операции вынести в отдельный scope.
- Rate limit — по
client_idи по пользователю; заголовкиRetry-After(8.05.122). - Webhooks — подпись, timestamp, идемпотентность по
evt id, быстрый ACK. - Версия — префикс
/v1/, политика sunset в документации. - Тесты — 7.05.2, контракт по OpenAPI.
Смежные материалы
| Тема | Статья |
|---|---|
| Первый сквозной пример (B2B, Idempotency-Key) | 7.06.117 |
| Аутентификация в интеграциях | 7.06.117 |
| Идемпотентность | 7.06.213 |
| Уязвимости API | 8.07.128 |
| Маршрут по всем главам | 7.06.117 |
| mTLS, JWS, AsyncAPI, outbox | 7.06.1172 |
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). Каждая система имеет свою архитектуру построения; систему нужно разворачивать под нагрузку; нужно понимать обновления и исправление ошибок; рано или поздно — интеграция, безопасность, расширение и поддержка. Подход к проектированию — это стратегия, которая определяет, откуда начинается работа над системой и в каком порядке формируются её компоненты. SOLID, DRY, KISS, YAGNI, закон Конвея и SOC — критерии оценки решений с примерами нарушений и чек-листами для поддерживаемого кода. Проектирование сервисов - от микросервисов до доменных сервисов в DDD и как не путать уровни ответственности. Любое действие пользователя — это запрос на изменение состояния, а не прямая команда. Как формулировать измеримые NFR и переводить их в архитектурные решения: масштабирование, отказоустойчивость, безопасность, observability. Традиционный подход: 1. Команда проектирует систему, 2. Пишет код, 3. По завершении — создаёт документацию для сдачи заказчику или архивирования. Проектирование баз данных — это системная инженерная дисциплина, направленная на создание структуры хранения данных, которая обеспечивает корректность, целостность, производительность, расширяемость. Современные программные системы редко существуют изолированно. Третий сквозной пример — доверенный B2B-контур (mTLS), подпись webhooks через JWS, контракт событий в AsyncAPI и надёжная публикация через transactional outbox. Декомпозиция, API Gateway, database per service, Saga, observability и антипаттерны — практика микросервисов. Переходите к изучению этой статьи только после того, как изучите микросервисы.Проектирование программных систем
Подходы к проектированию
Принципы проектирования
Проектирование сервисов и методов
Проектирование функциональных UI
Проектирование под нефункциональные требования
Документация как инструмент проектирования
Проектирование баз данных
Проектирование API и интеграций
mTLS, JWS-подпись webhooks и AsyncAPI с outbox
Паттерны микросервисной архитектуры
Проектирование веб-разработки