Тестирование OAuth 2.0 и JWT: как проверять токены, истечение сессий и edge-кейсы авторизации

Коротко:

  • OAuth 2.0 - протокол делегированной авторизации с несколькими флоу; JWT - формат токена с подписью и полезной нагрузкой. Их тестируют по-разному, но в связке.
  • Главное, что пропускают: проверка поведения при истёкшем токене, корректность обновления сессии и обработка подмены payload.
  • Большинство edge-кейсов не видно в happy path - они всплывают при граничных значениях времени, повторных запросах и нестандартных сценариях выхода.
  • Для ручного разбора структуры удобен jwt.io; для автоматизации - Postman pre-request scripts или pytest с библиотекой PyJWT.
  • Refresh token требует отдельного набора проверок: одноразовость, ротация, поведение после отзыва.

Зачем это вообще тестировать отдельно

OAuth 2.0 и JWT встречаются почти в каждом современном продукте - от мобильных приложений до корпоративных порталов. При этом большинство команд проверяют только happy path: пользователь залогинился, токен пришёл, запрос прошёл. Этого мало.

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

Эта статья - про конкретные сценарии. Что именно проверять, где обычно ломается и как выстроить покрытие так, чтобы не полагаться на удачу.

Как устроен JWT: что важно знать для тестирования

JSON Web Token состоит из трёх частей, разделённых точками: header, payload и signature. Header содержит алгоритм подписи (например, HS256 или RS256). Payload - произвольные поля: идентификатор пользователя, роли, время выдачи (iat), время истечения (exp). Signature подтверждает, что токен не изменяли.

Важный нюанс: payload закодирован в base64, но не зашифрован. Любой, у кого есть токен, может прочитать содержимое - просто вставив его в jwt.io. Это значит, что в payload нельзя класть секреты: пароли, платёжные данные, внутренние ключи.

Для тестировщика это открывает возможности: можно посмотреть, что реально выдаёт сервер, какие роли указаны, какое время истечения установлено - и сравнить с ожидаемым поведением.

Флоу OAuth 2.0: что и когда нужно проверять

OAuth 2.0 описывает несколько сценариев получения доступа (grants). Чаще всего встречаются три:

  • Authorization Code - пользователь логинится через браузер, получает code, который обменивается на токены. Используется в веб-приложениях.
  • Client Credentials - сервис-к-сервису, без участия пользователя. Нет ни code, ни refresh token.
  • PKCE (Proof Key for Code Exchange) - расширение Authorization Code для мобильных и SPA-приложений.

Каждый флоу имеет свои граничные случаи. Authorization Code предполагает одноразовый code - его нельзя использовать дважды. PKCE добавляет code_verifier и code_challenge, которые тоже нужно проверять.

Спецификация OAuth 2.0 описана в RFC 6749, а PKCE - в RFC 7636. Это полезные источники, когда нужно понять, как система должна себя вести по стандарту.

Проверка структуры и содержимого токена

Первый шаг - убедиться, что токен вообще корректен по структуре. Невалидный JWT (лишние символы, неверное количество сегментов) должен отклоняться с 401, а не вызывать 500.

Дальше - содержимое payload. Что именно проверять:

  • exp - есть ли поле и разумное ли значение. Токен без exp никогда не истекает - это баг безопасности.
  • iat - время выдачи. Помогает понять, не выдан ли токен в прошлом.
  • aud - аудитория. Токен, выданный для одного сервиса, не должен приниматься другим.
  • sub - идентификатор субъекта. Убедитесь, что это идентификатор нужного пользователя, а не соседнего.
  • Кастомные claims: роли, разрешения, tenant_id - всё, что добавляет ваша система.

Удобный способ посмотреть payload вручную - скопировать токен в jwt.io. Для автотестов используйте PyJWT (Python) или jsonwebtoken (Node.js): они позволяют декодировать и верифицировать содержимое программно.

Пример: Представим SaaS-продукт с несколькими тарифами. В payload хранится поле plan: "free". Если сервер не перепроверяет права на своей стороне и доверяет только токену, пользователь может вручную изменить payload (подделав подпись) и получить доступ к premium-функциям. Проверяйте, что backend валидирует подпись, а не просто декодирует содержимое.

Истечение сессии: граничные кейсы по времени

Поле exp задаёт временную метку в формате Unix timestamp. Токен считается истёкшим, когда текущее время сервера превышает это значение. Казалось бы, просто - но здесь скрыто несколько неочевидных ситуаций.

Что проверять:

  • Запрос с токеном, у которого exp прошёл 1 секунду назад. Сервер должен вернуть 401, а не 200.
  • Запрос прямо на границе истечения (clock skew). Если у клиента и сервера разные часы, возможны ложные отказы или, наоборот, принятие истёкшего токена. RFC 7519 допускает небольшой leeway (обычно не более 5 минут), но это решение на стороне сервера.
  • Токен с exp в далёком прошлом (1970-й год или вчерашний день). Должен отклоняться.
  • Токен без поля exp вообще. Идеальный ответ - 401 с понятным сообщением об ошибке.

Для ручного тестирования удобно вручную генерировать токены с нужными значениями exp через jwt.io (с известным секретом на тестовом стенде) или через Postman pre-request script.

Pre-request script в Postman для генерации истёкшего токена:

const jwt = require('jsonwebtoken');
const secret = 'test-secret';
const token = jwt.sign(
  { sub: '123', exp: Math.floor(Date.now() / 1000) - 60 },
  secret
);
pm.environment.set('expired_token', token);

Postman поддерживает require('jsonwebtoken') через встроенные библиотеки. Готовый токен подставляется в заголовок Authorization автоматически.

Тестирование refresh token

Refresh token - долгоживущий артефакт, который позволяет получить новый access token без повторного логина. Именно здесь сосредоточено больше всего потенциально критичных ошибок.

СценарийОжидаемое поведениеЧастая ошибка
Использование валидного refresh tokenНовый access token + новый refresh token (при ротации)Старый refresh token остаётся активным
Повторное использование уже применённого refresh token401 + инвалидация всей сессииСервер выдаёт новый токен без проверки
Refresh token после явного выхода пользователя401Токен остаётся действующим
Истёкший refresh token401, пользователь перенаправляется на логин500 или зависание запроса
Refresh token от другого пользователя401Принимается и выдаёт чужой access token

Концепция refresh token rotation описана в документации Auth0: при каждом обновлении старый refresh token инвалидируется, выдаётся новый. Повторное использование старого должно сигнализировать о потенциальной компрометации и блокировать всю сессию.

Edge-кейсы авторизации: что чаще всего пропускают

Стандартный набор проверок покрывает очевидные пути. Edge-кейсы - это ситуации, которые в обычном сценарии не встречаются, но случаются у реальных пользователей или при атаке.

Подмена алгоритма (algorithm confusion)

В поле alg заголовка JWT можно указать none. Некоторые библиотеки принимают такой токен без проверки подписи. Это классическая атака, описанная в PortSwigger Web Security Academy. Проверьте: если отправить токен с alg: none и пустой подписью, сервер должен вернуть 401.

Схожий вариант - смена алгоритма с RS256 (асимметричный) на HS256 (симметричный). Если сервер принимает оба, можно попробовать подписать токен публичным ключом как секретом для HS256.

Параллельные сессии и инвалидация

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

Scope и aud

Токен с правами на чтение не должен давать права на запись. Токен, выданный для сервиса A, не должен приниматься сервисом B. Это проверяется просто: берёте токен из одного контекста и используете его в другом. Ответ должен быть 403 (недостаточно прав) или 401 (неверная аудитория).

Отзыв токена (token revocation)

JWT по природе своей stateless: сервер не хранит список выданных токенов. Поэтому отзыв - нетривиальная задача. Если в вашей системе есть logout или принудительная инвалидация, проверьте: токен, добавленный в blacklist, должен отклоняться, даже если его exp ещё не наступил. Механизм описан в RFC 7009.

Конкурентные запросы на обновление

Два параллельных запроса с одним refresh token - что вернёт сервер? Если оба проходят и возвращают разные access token, это гонка состояний. Такой сценарий реален для мобильных приложений, которые делают несколько параллельных API-запросов при запуске.

Важно: Не тестируйте edge-кейсы безопасности на продакшн-среде. Используйте стейджинг или локальный стенд с изолированной базой данных и отдельными ключами подписи.

Инструменты: что и для чего

ИнструментДля чего подходитКогда использовать
jwt.ioДекодирование и ручная проверка структурыБыстрый разбор токена, ручные проверки payload
Postman + pre-request scriptsАвтоподстановка токенов, тестирование refresh-флоуРучное и полуавтоматизированное тестирование API
PyJWT / jsonwebtokenГенерация и верификация токенов в автотестахpytest, Jest, CI-пайплайн
Burp Suite / OWASP ZAPПерехват и модификация токеновПроверка уязвимостей (alg confusion, подмена claims)
NewmanЗапуск Postman-коллекций в CIАвтоматизация регрессии по авторизации

Для большинства проверок достаточно Postman и jwt.io. Burp Suite нужен, если вы целенаправленно ищете уязвимости, а не просто проверяете корректность поведения.

Как выстроить автотесты для авторизационного флоу

Автотесты для OAuth 2.0 обычно разбивают на несколько уровней.

Unit-уровень - проверка самой логики валидации токена на стороне бэкенда: правильно ли код отклоняет истёкшие подписи, токены с неверным aud, токены без exp. Это быстрые тесты без сети.

Integration-уровень - реальные HTTP-запросы к авторизационному серверу. Получили токен, использовали, получили ожидаемый ответ. Здесь же - тест refresh-флоу: получить пару токенов, дождаться (или сдвинуть) истечения access token, обновить через refresh, убедиться, что новый работает.

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

Пример на pytest + PyJWT (integration):

import jwt
import requests

def test_expired_token_returns_401():
    secret = 'test-secret'
    expired = jwt.encode(
        {'sub': '42', 'exp': 1},  # exp в далёком прошлом
        secret,
        algorithm='HS256'
    )
    response = requests.get(
        'https://api.example.com/protected',
        headers={'Authorization': f'Bearer {expired}'}
    )
    assert response.status_code == 401

Главное в автотестах - не проверять только статус-код. Проверяйте тело ответа: есть ли понятное сообщение об ошибке, правильный ли error в теле (например, token_expired или invalid_token). Это важно для клиентских приложений, которые разбирают ошибки.

Типичные ошибки при тестировании

Проверять только happy path. Токен выдан - работает. Это закрывает 20% возможных проблем. Основная масса багов живёт в граничных состояниях: истечение, отзыв, параллельные запросы.

Не проверять содержимое токена. QA убедился, что запрос прошёл с кодом 200, но не посмотрел, что именно вернул сервер в payload. В итоге пользователь с ролью viewer получает токен с ролью admin - и это никто не замечает до инцидента.

Игнорировать поведение после logout. Пользователь нажал «Выйти». Если при этом refresh token не инвалидируется на сервере, его можно использовать повторно. Проверяйте это явно: после выхода попробуйте обновить сессию с тем же refresh token.

Не тестировать clock skew. На стейджинге часы сервера и клиента синхронизированы. В продакшне - нет. Если сервер не учитывает небольшое расхождение, пользователи с неточными часами будут получать 401 без видимой причины.

Пропускать проверку aud. В микросервисной архитектуре токен от сервиса A не должен принимать сервис B. Это проверяется за пять минут, но часто упускается.

Чеклист для QA

  1. Токен содержит поле exp с разумным значением.
  2. Истёкший токен возвращает 401, а не 200 или 500.
  3. Токен без подписи или с невалидной подписью отклоняется.
  4. Токен с alg: none отклоняется.
  5. Payload содержит корректные роли и идентификатор пользователя.
  6. Refresh token работает один раз (при включённой ротации).
  7. Повторное использование уже применённого refresh token возвращает 401.
  8. После logout refresh token инвалидируется на сервере.
  9. Смена пароля завершает все активные сессии.
  10. Токен с неверным aud отклоняется целевым сервисом.
  11. Параллельные запросы на обновление не создают гонку состояний.
  12. Сообщения об ошибках информативны и не раскрывают внутренние детали реализации.

Часто задаваемые вопросы

Access token - короткоживущий (обычно 15 минут - 1 час), используется в каждом запросе. Refresh token - долгоживущий (дни или недели), хранится безопаснее и используется только для получения нового access token. Тестируются они по-разному: у access token проверяют корректность claims и обработку истечения, у refresh token - одноразовость, ротацию и поведение после отзыва.

Скопируйте токен и вставьте вторую часть (между первой и второй точкой) в любой base64-декодер. Вы получите JSON с payload. Для проверки подписи нужен jwt.io или библиотека с известным секретом - просто декодирование не подтверждает валидность.

PKCE (Proof Key for Code Exchange) - расширение для защиты Authorization Code флоу в мобильных и SPA-приложениях. При тестировании проверяют, что code_verifier и code_challenge соответствуют друг другу, и что authorization code не принимается без правильного verifier.

Используйте тестовый стенд с собственным секретом. На продакшн-среде генерировать подписанные токены вручную не нужно - там достаточно проверять поведение системы через реальные запросы. Для edge-кейсов с поддельными токенами нужен изолированный стенд.

По стандарту (RFC 6750) для ошибок авторизации используется 401 Unauthorized с заголовком WWW-Authenticate . Для ошибок доступа (токен валиден, но прав недостаточно) - 403 Forbidden. Смешение этих кодов - тоже баг, который стоит фиксировать.

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

Итог

Большинство проблем с авторизацией возникают не из-за сложных атак, а из-за непроверенных граничных состояний: токен без срока жизни, refresh token который можно использовать дважды, сессия которая не завершается после выхода. Всё это находится ручным тестированием за несколько часов - если знать, где смотреть.

Хорошее покрытие строится на трёх слоях: ручная проверка структуры через jwt.io, интеграционные тесты с граничными значениями времени и статусов, и явная проверка отзыва токенов после logout и смены пароля. Именно эти сценарии чаще всего остаются вне тест-плана - и именно они всплывают в продакшне.