Тестирование аутентификации
Updated Jul 2026
Аутентификация — привратник вашего API. Тщательное тестирование означает проверку не только того, что валидные учётные данные работают, но и того, что невалидные учётные данные корректно отклоняются, токены истекают как ожидается, а конфиденциальная информация никогда не утекает в ответах об ошибках.
Методы аутентификации
| Метод | Как работает | Где распространён |
|---|---|---|
| API Key | Статический ключ в заголовке или параметре запроса | Сторонние интеграции, внутренние API |
| OAuth 2.0 | Поток обмена токенами (authorization code, client credentials) | SaaS-платформы, социальный логин |
| JWT | Самодостаточный подписанный токен с утверждениями | Современные API, микросервисы |
| Session Cookie | Серверная сессия с ID в куке | Традиционные веб-приложения |
| Basic Auth | Base64-кодированные username:password в заголовке | Устаревшие API, простые внутренние инструменты |
Тестирование каждого метода
Тестирование API Key
def test_valid_api_key(base_url):
r = requests.get(f"{base_url}/data",
headers={"X-API-Key": os.environ["VALID_API_KEY"]})
assert r.status_code == 200
def test_missing_api_key(base_url):
r = requests.get(f"{base_url}/data")
assert r.status_code == 401
def test_invalid_api_key(base_url):
r = requests.get(f"{base_url}/data",
headers={"X-API-Key": "invalid-key-12345"})
assert r.status_code == 401
# Should not reveal whether the key format is wrong vs not found
assert "valid key" not in r.text.lower()
def test_revoked_api_key(base_url, revoked_key):
r = requests.get(f"{base_url}/data",
headers={"X-API-Key": revoked_key})
assert r.status_code == 401
Тестирование JWT
def test_valid_jwt(base_url, valid_token):
r = requests.get(f"{base_url}/users/me",
headers={"Authorization": f"Bearer {valid_token}"})
assert r.status_code == 200
def test_expired_jwt(base_url, expired_token):
r = requests.get(f"{base_url}/users/me",
headers={"Authorization": f"Bearer {expired_token}"})
assert r.status_code == 401
def test_tampered_jwt(base_url, valid_token):
"""Modify the payload of a valid JWT — should be rejected."""
import base64
parts = valid_token.split(".")
# Decode payload, modify it, re-encode (signature will be invalid)
payload = base64.urlsafe_b64decode(parts[1] + "==")
tampered = valid_token.replace(parts[1], base64.urlsafe_b64encode(
payload.replace(b'"role":"viewer"', b'"role":"admin"')
).decode().rstrip("="))
r = requests.get(f"{base_url}/users/me",
headers={"Authorization": f"Bearer {tampered}"})
assert r.status_code == 401
def test_missing_auth_returns_401(base_url):
r = requests.get(f"{base_url}/users/me")
assert r.status_code == 401
assert "password" not in r.text.lower() # No info leakage
def test_malformed_auth_header(base_url):
r = requests.get(f"{base_url}/users/me",
headers={"Authorization": "NotBearer token123"})
assert r.status_code == 401
Поток обновления токена
def test_token_refresh(base_url, refresh_token):
"""Refresh token should return a new access token."""
r = requests.post(f"{base_url}/auth/refresh",
json={"refresh_token": refresh_token})
assert r.status_code == 200
new_token = r.json()["access_token"]
assert new_token != ""
# New token should work
r = requests.get(f"{base_url}/users/me",
headers={"Authorization": f"Bearer {new_token}"})
assert r.status_code == 200
def test_used_refresh_token_invalidated(base_url, refresh_token):
"""After using a refresh token, it should be invalidated (one-time use)."""
r1 = requests.post(f"{base_url}/auth/refresh",
json={"refresh_token": refresh_token})
assert r1.status_code == 200
r2 = requests.post(f"{base_url}/auth/refresh",
json={"refresh_token": refresh_token})
assert r2.status_code == 401 # Reuse should fail
Тестирование сессионных кук
def test_session_cookie_attributes(base_url):
r = requests.post(f"{base_url}/auth/login", json={
"email": "test@example.com", "password": "pass123"
})
assert r.status_code == 200
# Verify cookie security attributes
session_cookie = r.cookies.get("session_id")
assert session_cookie is not None
# Check Set-Cookie header for attributes
set_cookie = r.headers.get("Set-Cookie", "")
assert "HttpOnly" in set_cookie # Not accessible via JavaScript
assert "Secure" in set_cookie # Only sent over HTTPS
assert "SameSite" in set_cookie # CSRF protection
Тестирование авторизации (RBAC)
Аутентификация проверяет личность. Авторизация проверяет разрешения. Тестируйте, что пользователи могут получить доступ только к тому, что позволяет их роль.
@pytest.mark.parametrize("role,endpoint,method,expected", [
("admin", "/users", "GET", 200),
("admin", "/users", "POST", 201),
("admin", "/users/1", "DELETE", 204),
("viewer", "/users", "GET", 200),
("viewer", "/users", "POST", 403),
("viewer", "/users/1", "DELETE", 403),
("editor", "/users", "GET", 200),
("editor", "/users", "POST", 201),
("editor", "/users/1", "DELETE", 403),
])
def test_role_based_access(base_url, get_token_for_role, role, endpoint, method, expected):
token = get_token_for_role(role)
r = requests.request(method, f"{base_url}{endpoint}",
headers={"Authorization": f"Bearer {token}"},
json={"name": "Test", "email": f"{role}@test.com"} if method == "POST" else None)
assert r.status_code == expected, \
f"Role {role} got {r.status_code} on {method} {endpoint}, expected {expected}"
Горизонтальная авторизация (IDOR)
Пользователи не должны получать доступ к данным других пользователей:
def test_user_cannot_access_other_users_data(base_url, user_a_token, user_b_id):
"""User A should not be able to view User B's private data."""
r = requests.get(f"{base_url}/users/{user_b_id}/private-data",
headers={"Authorization": f"Bearer {user_a_token}"})
assert r.status_code in (403, 404) # Either forbidden or not found
Проверки безопасности
def test_auth_error_no_info_leakage(base_url):
"""Error responses should not reveal whether email exists."""
r1 = requests.post(f"{base_url}/auth/login",
json={"email": "exists@test.com", "password": "wrong"})
r2 = requests.post(f"{base_url}/auth/login",
json={"email": "nonexistent@test.com", "password": "wrong"})
# Both should return the same error message (no user enumeration)
assert r1.json().get("message") == r2.json().get("message")
def test_brute_force_protection(base_url):
"""Multiple failed logins should trigger rate limiting or lockout."""
for i in range(10):
r = requests.post(f"{base_url}/auth/login",
json={"email": "target@test.com", "password": f"wrong{i}"})
if r.status_code == 429:
break
assert r.status_code == 429 or r.status_code == 423 # Rate limited or locked
Практическое упражнение
- Напишите тесты для потока логина: валидные учётные данные, невалидный пароль, невалидный email, пустые поля
- Протестируйте жизненный цикл JWT: получите токен, используйте его, дождитесь истечения, проверьте 401, обновите его
- Протестируйте RBAC: создайте фикстуры для ролей admin, editor и viewer; проверьте матрицу доступа
- Протестируйте IDOR: проверьте, что Пользователь А не может получить доступ к данным Пользователя Б
- Протестируйте утечку информации: проверьте, что сообщения об ошибках не раскрывают существование пользователя
Ключевые выводы
- Тестируйте каждый метод аутентификации: валидные, отсутствующие, невалидные, просроченные и подделанные учётные данные
- JWT-тесты должны покрывать истечение, подделку и потоки обновления
- Тесты авторизации (RBAC) проверяют границы ролей и разрешений
- IDOR-тесты проверяют, что пользователи не могут получить доступ к приватным данным других пользователей
- Сообщения об ошибках не должны содержать утечек информации (перечисление пользователей, стек-трейсы)
- Безопасность кук: проверяйте атрибуты HttpOnly, Secure и SameSite