БлогУслугиКарьера
Обсудить проект
БлогУслугиКарьераОбсудить проект
Автоматизация

OAuth 2.0 для интеграций: как подключить сайт к чужому сервису

Разбираем Authorization Code Flow с PKCE по шагам: где живут токены, зачем нужен state, почему падает redirect_uri и что делать, когда refresh протух.

Редакция Feature
Редакция Feature·
25 авг
·
14 мин
·
OAuth 2.0 для интеграций: как подключить сайт к чужому сервису

Пользователь нажимает «Подключить Google Календарь». Его перебрасывает на страницу Google, он соглашается, возвращается обратно — и в вашем интерфейсе появляются его встречи.

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

Разберём Authorization Code Flow по шагам, ровно в том виде, в каком его нужно писать в 2026 году, и отдельно пройдём по местам, где интеграции ломаются чаще всего.

Зачем OAuth, если есть API-ключ

Разница в том, чей доступ вы получаете.

API-ключ даёт ваш собственный доступ. Вы регистрируетесь в сервисе, получаете строку, кладёте её на сервер и ходите от своего имени. Так работает интеграция с курсами валют или отправкой SMS.

OAuth даёт доступ от имени пользователя. Вам нужны встречи конкретного человека, его файлы или его заказы. Свой ключ тут не поможет: у вас нет прав на чужие данные, и быть их не должно.

Отсюда и вся сложность протокола. Пользователь должен подтвердить согласие в сервисе, которому доверяет, а вы — получить ограниченный, отзываемый доступ, не увидев его пароль.

Именно поэтому просить у пользователя логин и пароль от стороннего сервиса нельзя. Такой способ (password grant) в протоколе был, но признан устаревшим и из современных версий протокола убран.

Роли: кто есть кто

Четыре участника, без академизма:

  • Пользователь, владелец данных: тот, кто нажимает «Разрешить»
  • Ваше приложение, клиент: хочет получить доступ
  • Сервер авторизации: страница согласия и выдача токенов
  • Сервер ресурсов: собственно API с данными

Сервер авторизации и сервер ресурсов часто живут на разных доменах, и это нормально: согласие спрашивается в одном месте, данные лежат в другом.

Шаг 1. Готовим PKCE

PKCE (произносится «пикси») изначально придумали для мобильных приложений, а теперь он обязателен для всех. Смысл в защите от перехвата: даже если кто-то украдёт код авторизации из адресной строки, обменять его на токен без секрета, который остался у вас, не выйдет.

Работает это так: вы генерируете случайную строку code_verifier, отправляете на сервер её хеш, а сам verifier предъявляете только на последнем шаге.

// Web Crypto доступен и в браузере, и в Node 18+
function base64url(bytes: Uint8Array): string {
  return btoa(String.fromCharCode(...bytes))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));

const digest = await crypto.subtle.digest(
  "SHA-256",
  new TextEncoder().encode(verifier)
);
const challenge = base64url(new Uint8Array(digest));

Метод хеширования обязательно S256. Вариант plain, при котором challenge равен verifier, поддерживается для совместимости и не защищает ни от чего.

Шаг 2. Отправляем пользователя за согласием

Собираем ссылку на сервер авторизации. Обратите внимание на state: к нему вернёмся отдельно, это не декоративный параметр.

// app/api/oauth/start/route.ts
import { cookies } from "next/headers";

export async function GET() {
  const state = base64url(crypto.getRandomValues(new Uint8Array(16)));
  // verifier и state нужны на колбэке — кладём в httpOnly-куку, не в localStorage
  const jar = await cookies();
  jar.set("oauth_verifier", verifier, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    maxAge: 600,
  });
  jar.set("oauth_state", state, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    maxAge: 600,
  });

  const url = new URL("https://accounts.example.com/o/oauth2/v2/auth");
  url.searchParams.set("client_id", process.env.OAUTH_CLIENT_ID!);
  url.searchParams.set("redirect_uri", "https://example.ru/api/oauth/callback");
  url.searchParams.set("response_type", "code");
  url.searchParams.set("scope", "calendar.readonly");
  url.searchParams.set("state", state);
  url.searchParams.set("code_challenge", challenge);
  url.searchParams.set("code_challenge_method", "S256");

  return Response.redirect(url.toString());
}

Про scope стоит сказать отдельно. Запрашивайте минимум, который нужен прямо сейчас. Широкий набор прав пугает пользователей на экране согласия и увеличивает ущерб при утечке токена. Права всегда можно допросить позже, отдельным заходом.

Нужно подключить сторонний сервис?

Спроектируем интеграцию с чужим API и возьмём на поддержку

Обсудить интеграцию

Шаг 3. Меняем код на токены

Пользователь согласился, сервис вернул его на ваш redirect_uri с параметром code. Этот код одноразовый и живёт около минуты.

// app/api/oauth/callback/route.ts
export async function GET(req: Request) {
  const params = new URL(req.url).searchParams;
  const code = params.get("code");
  const returnedState = params.get("state");

  const jar = await cookies();
  const savedState = jar.get("oauth_state")?.value;
  const verifier = jar.get("oauth_verifier")?.value;

  // Проверка state обязательна — см. следующий раздел
  if (!code || !returnedState || returnedState !== savedState || !verifier) {
    return new Response("invalid oauth state", { status: 400 });
  }

  const res = await fetch("https://oauth2.example.com/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: "https://example.ru/api/oauth/callback",
      client_id: process.env.OAUTH_CLIENT_ID!,
      client_secret: process.env.OAUTH_CLIENT_SECRET!,
      code_verifier: verifier,
    }),
    signal: AbortSignal.timeout(8000),
  });

  if (!res.ok) return new Response("token exchange failed", { status: 502 });

  const tokens = await res.json(); // access_token, refresh_token, expires_in
  // сохранить в свою базу, привязав к пользователю
}

Тело запроса здесь именно application/x-www-form-urlencoded, а не JSON. Это одна из самых частых причин загадочного 400 invalid_request на первом подключении.

Где ломается чаще всего

state, которого нет

Параметр state защищает от подделки межсайтового запроса. Без него злоумышленник может подсунуть пользователю ссылку возврата со своим кодом авторизации — и аккаунт жертвы в вашем сервисе окажется привязан к его аккаунту в стороннем сервисе.

Проверка занимает одну строку, а пропускают её постоянно, потому что без неё всё работает. Ошибка не проявляется никак, пока ею не воспользуются.

redirect_uri не совпадает

Второе место по частоте. Сервер авторизации сверяет адрес возврата посимвольно с тем, что записан в настройках приложения. Не совпадает что угодно из этого:

  • лишний или недостающий слеш в конце
  • http вместо https
  • www.example.ru против example.ru
  • другой порт на локальной разработке

Заведите отдельные приложения для разработки и продакшена, а адрес возврата держите в переменной окружения, а не в коде.

Токены в localStorage

access_token в localStorage доступен любому скрипту на странице. Достаточно одного постороннего скрипта на странице или одной скомпрометированной зависимости, чтобы токен утёк.

Правильное место для токенов стороннего сервиса на вашем сервере. Браузер вообще не должен их видеть: фронтенд ходит на ваш эндпоинт, а тот уже подставляет токен и обращается к чужому API. Тот же приём, что и с обычными ключами доступа.

refresh-токен обновляется параллельно

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

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

Токены: сколько живут и что делать, когда протухли

access_token короткоживущий, обычно от нескольких минут до часа. Его протухание считается штатной ситуацией, а не ошибкой.

refresh_token живёт долго, но не вечно. Он перестаёт работать, если пользователь отозвал доступ в настройках сервиса, сменил пароль, если токеном давно не пользовались или администратор отключил приложение.

Из этого следует практическое правило: отказ по refresh-токену нужно обрабатывать как отдельный сценарий, а не как сбой. Пользователю показывают понятное «Подключение к сервису прервалось, авторизуйтесь заново» и кнопку. Интеграция, которая в этот момент просто пишет в лог 401, выглядит для человека как молча сломавшийся продукт.

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

Обсудим ваш проект?

Оставьте контакты — перезвоним и обсудим задачу

Чего делать не нужно

Implicit flow. Схема, при которой токен приходил прямо в адресной строке. Устарела, из современной версии протокола убрана. Если в чужой документации он всё ещё описан, берите Authorization Code с PKCE.

Логин и пароль пользователя. Уже упоминали, но повторим: это ровно то, чего OAuth призван избежать.

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

Чек-лист подключения

  • Authorization Code Flow с PKCE, метод S256
  • state генерируется и проверяется на колбэке
  • verifier и state лежат в httpOnly-куках, а не в localStorage
  • redirect_uri совпадает посимвольно, для дева и прода разные приложения
  • Обмен кода идёт с Content-Type: application/x-www-form-urlencoded
  • Токены хранятся на сервере, браузер их не видит
  • Обновление refresh-токена защищено от параллельных вызовов
  • Протухший refresh обрабатывается как «переподключитесь», а не как ошибка
  • При отключении интеграции токен отзывается на стороне сервиса
  • Запрошен минимально необходимый набор прав

Заключение

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

Собрать рабочий обмен можно за день. Разница между «работает» и «работает правильно» проходит по четырём местам: проверка state, PKCE, хранение токенов на сервере и одиночное обновление refresh. Всё остальное сводится к деталям конкретного сервиса, которые всегда есть в его документации.

Читайте также

  • Настройка API на сайте: 7 ошибок, которые вылезают только в проде
  • Интеграция 1С с сайтом через API: полный гайд для бизнеса
  • Требования 152-ФЗ к сайту

Обсудим ваш проект?

Оставьте контакты — перезвоним и обсудим задачу

Содержание
  • Зачем OAuth, если есть API-ключ
  • Роли: кто есть кто
  • Шаг 1. Готовим PKCE
  • Шаг 2. Отправляем пользователя за согласием
  • Шаг 3. Меняем код на токены
  • Где ломается чаще всего
  • Токены: сколько живут и что делать, когда протухли
  • Чего делать не нужно
  • Чек-лист подключения
  • Заключение
Поделиться:

Похожие статьи

Свои вебхуки: как отдавать события партнёрам и не потерять их
Автоматизация

Свои вебхуки: как отдавать события партнёрам и не потерять их

13 мин
Настройка API на сайте: 7 ошибок, которые вылезают только в проде
Автоматизация

Настройка API на сайте: 7 ошибок, которые вылезают только в проде

13 мин
5 новейших ИИ-моделей 2026: кто полезнее в реальной работе
Автоматизация

5 новейших ИИ-моделей 2026: кто полезнее в реальной работе

10 мин
Feature IT

Feature IT — платформа по обучению программированию и разработке цифровых продуктов. Мы создаём современные веб-решения для бизнеса и обучаем этому других!

Политика конфиденциальностиПользовательское соглашение

О компании

  • Блог
  • Карьера

Услуги разработки

  • Разработка сайтов под ключ
  • Веб-приложения на React/Next.js
  • Telegram-боты для бизнеса
  • Mini Apps (Telegram, VK)
  • SEO-оптимизированные сайты
  • Автоматизация бизнес-процессов
  • Поддержка и развитие IT-продуктов

Обучение

  • Курс Python с нуля
  • Алгоритмы и структуры данных
  • Паттерны проектирования
  • Подготовка к собеседованиям в IT
  • Практика на реальных проектах

Инструменты

  • Генератор UTM-меток
  • Счётчик символов