Когда ты строишь API, первый вопрос, который встаёт после «как это всё вообще работает» — это «а кто вообще имеет право сюда ходить». Аутентификация — это не про безопасность ради безопасности, это про базовую гигиену. Без неё любой желающий дёрнет твой endpoint, и хорошо, если просто почитает данные, а не удалит или украдёт.

Есть три подхода, которые используются чаще всего: API-ключи, JWT и OAuth2. Они разные по сложности, по задачам и по тому, где ломаются. Разберём каждый нормально.

API-ключи — просто, грубо, но работает

API-ключ — это строка вроде sk-a3f9b2c1d4e5f678. Ты её генеришь, отдаёшь клиенту, он прикладывает к каждому запросу. Всё.

Обычно ключ передаётся в заголовке:

Authorization: Bearer sk-a3f9b2c1d4e5f678

Или в параметре запроса — но это хуже, потому что ключ попадёт в логи:

GET /api/data?api_key=sk-a3f9b2c1d4e5f678

Где это работает хорошо: server-to-server интеграции, когда у тебя есть сервис, который дёргает другой сервис, и оба под твоим контролем. OpenAI так устроен, Stripe так устроен, большинство SaaS-платформ — тоже.

Где ломается: если ключ утёк — всё, нужно его инвалидировать и выдать новый. Ключ не несёт никакой информации о пользователе — это просто пропуск. Если ты хочешь знать, кто именно к тебе пришёл, придётся делать lookup в базе при каждом запросе.

Ещё один минус — ротация. Если у тебя 50 клиентов с ключами, и один ключ скомпрометирован — ты не знаешь, кто с ним ходил, пока не поднял логи. Это больно.

Когда брать: внутренние интеграции, инструменты для разработчиков, B2B-API с небольшим числом клиентов.

JWT — токен, который сам о себе всё знает

JWT (JSON Web Token) — это не просто строка-пропуск, это токен с данными внутри. Выглядит страшно:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiSXZhbm92IiwiaWF0IjoxNzEzODI0MDAwLCJleHAiOjE3MTM4Mjc2MDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Но на самом деле это три части через точку: заголовок, payload и подпись. Payload — это JSON с данными, который можно декодировать (это не шифрование, а кодирование base64url). Там лежит что-то вроде:

{
  "sub": "123456",
  "name": "Иванов",
  "role": "admin",
  "iat": 1713824000,
  "exp": 1713827600
}

Подпись сервер проверяет с помощью секретного ключа. Если кто-то попытается изменить payload — подпись не совпадёт, запрос отклонится.

Главное преимущество JWT: сервер не хранит сессии. Токен пришёл — сервер его проверил подписью, достал данные из payload, пошёл дальше. Никаких обращений к базе. Это делает JWT отличным выбором для горизонтально масштабируемых систем — не важно, на какой инстанс попадёт запрос, токен проверят везде одинаково.

Подводные камни JWT

Первый — токен нельзя «отозвать» до истечения срока. Если пользователь поменял пароль или тебе нужно срочно выбить сессию — с обычным JWT ты ничего не сделаешь до exp. Решение: либо хранить blacklist токенов (и тогда ты снова ходишь в базу), либо делать очень короткие сроки жизни — 15 минут, час.

Второй — алгоритм none. Это баг в реализациях, который позволял атакующему указать в заголовке "alg": "none" и сервер принимал токен без проверки подписи. Сейчас нормальные библиотеки это запрещают, но если ты пишешь свою реализацию — не делай так.

Третий — размер. JWT может вырасти, если ты запихнёшь в payload слишком много данных. Он передаётся с каждым запросом, и это начинает ощутимо давить на трафик.

Схема с refresh-токенами: access-токен живёт 15 минут, refresh-токен — 30 дней и хранится в httpOnly cookie. Когда access протух — клиент тихо идёт за новым с refresh. Это стандарт для большинства веб-приложений.

Когда брать: веб-приложения, мобильные приложения, микросервисы, где сервисы не хотят ходить к центральному хранилищу сессий.

OAuth2 — когда нужно делегировать доступ

OAuth2 — это не просто способ аутентификации, это протокол делегирования доступа. Та кнопка «Войти через Google» или «Войти через VK» — это OAuth2.

Суть: пользователь разрешает твоему приложению делать что-то от его имени в другом сервисе. Не отдаёт пароль — а именно делегирует конкретный набор прав (scope).

Вот упрощённый flow Authorization Code (самый распространённый):

  1. Пользователь нажимает «Войти через Google»
  2. Твоё приложение редиректит его на Google с параметрами: client_id, redirect_uri, scope=email profile, state=случайная_строка
  3. Пользователь логинится в Google и разрешает доступ
  4. Google редиректит обратно с code
  5. Твой бэкенд меняет code на access_token и refresh_token (server-to-server, без пользователя)
  6. С access_token идёшь в Google API за данными

Параметр state — это защита от CSRF. Ты генеришь случайную строку, кладёшь в сессию, отправляешь в запросе. Когда Google вернул code — проверяешь, что state совпадает. Без этого атакующий может подсунуть чужой code.

Разные grant types

Authorization Code — для веб и мобильных приложений с бэкендом. Самый безопасный.

Authorization Code + PKCE — для мобильных и SPA, где нет бэкенда для обмена кода. PKCE (Proof Key for Code Exchange) добавляет code_verifier и code_challenge, чтобы перехватить code было бесполезно.

Client Credentials — для server-to-server без пользователя. Сервис A говорит: «Я — сервис A, вот мой client_id и client_secret, дай токен». Это OAuth2-аналог API-ключей, но с возможностью управлять scope.

Resource Owner Password — пользователь отдаёт логин/пароль напрямую твоему приложению, оно само получает токен. Считается устаревшим и опасным — не используй, если не вынуждают.

OpenID Connect поверх OAuth2

OAuth2 говорит «можно ли делать X», но не говорит «кто ты». OpenID Connect (OIDC) добавляет id_token — это JWT с данными о пользователе. Это то, что используется, когда тебе нужно «войти через Google» и узнать email пользователя.

Когда брать OAuth2: когда нужна социальная авторизация, когда строишь платформу с третьими разработчиками, когда нужно гранулированное управление доступом с scope.

Сравниваем всё вместе

API-ключи JWT OAuth2
Сложность реализации Низкая Средняя Высокая
Stateless Нет (lookup) Да Частично
Делегирование Нет Нет Да
Отзыв токена Легко Сложно Через refresh
Подходит для Server-to-server Веб/мобайл Третьи сервисы

На практике эти подходы часто комбинируют. Например: OAuth2 для аутентификации пользователей, JWT как формат access-токена, а API-ключи для внутренних сервисов.

Что точно не делать

Не передавай токены в URL. GET-параметры попадают в логи сервера, в историю браузера, в реферер при переходе на другой сайт. Используй заголовок Authorization.

Не храни токены в localStorage. Любой XSS скрипт прочитает его. Для access-токенов с коротким сроком жизни — можно в памяти, для refresh — httpOnly cookie.

Не подписывай JWT симметричным ключом, если несколько сервисов проверяют токен. Если сервис может проверять токен, он может и выпускать. Используй RS256 (асимметричную подпись): приватный ключ только у сервиса аутентификации, публичный — у всех.

Не делай длинные access-токены. 15 минут — нормально. Час — максимум. Refresh-токены ротируй: при каждом использовании выдавай новый, старый инвалидируй.

Не игнорируй exp и nbf. exp — токен истёк, nbf (not before) — токен ещё не активен. Проверяй оба.

Практический пример: что выбрать для конкретного проекта

Допустим, ты строишь мобильное приложение с бэкендом:

  • Авторизация пользователей: OAuth2 + OIDC (или просто JWT с username/password, если не нужна социальная авторизация)
  • Токены: access JWT 15 минут + refresh 30 дней в httpOnly cookie
  • Внутренние сервисы (например, сервис уведомлений вызывает API): Client Credentials или API-ключи

Допустим, ты строишь публичный API для разработчиков:

  • Авторизация разработчиков: API-ключи с привязкой к аккаунту
  • Если им нужно действовать от имени их пользователей: OAuth2 Authorization Code + PKCE

Допустим, ты строишь микросервисы:

  • Service mesh с mTLS или JWT с RS256, где identity provider выдаёт токены сервисам
  • Client Credentials для сервисных аккаунтов

Когда мы в REEXY проектируем API для клиентских проектов — часто начинаем с простого JWT, а OAuth2 добавляем тогда, когда реально нужна интеграция с третьими сервисами или делегирование. Усложнять сразу смысла нет.

Библиотеки, которые можно брать

Для JWT:

  • Node.js: jsonwebtoken, jose
  • Python: python-jose, PyJWT
  • Go: golang-jwt/jwt
  • Java: io.jsonwebtoken

Для OAuth2 (клиентская часть):

  • Node.js: passport, openid-client
  • Python: authlib
  • Go: golang.org/x/oauth2

Для OAuth2 (сервер авторизации):

  • Keycloak — тяжёлый, но мощный
  • Auth0, Okta — SaaS, быстро, но деньги
  • Ory Hydra — open source, Go
  • Authentik — self-hosted, хорошая альтернатива Keycloak

Итого

Выбор метода аутентификации зависит не от моды, а от задачи.

API-ключи — когда клиентов мало, доступ статичный, всё под твоим контролем.

JWT — когда нужен stateless, масштабируемый бэкенд с короткоживущими токенами.

OAuth2 — когда нужно делегирование, социальная авторизация или открытая платформа для сторонних разработчиков.

Чаще всего в реальном проекте всё три сосуществуют — и это нормально. Главное — понимать, зачем каждый из них здесь.