Modern QA2026Тестирование аутентификации
Join

Course14 API Testing Fundamentals

Foundations · Chapter 14

Тестирование аутентификации

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

Практическое упражнение

  1. Напишите тесты для потока логина: валидные учётные данные, невалидный пароль, невалидный email, пустые поля
  2. Протестируйте жизненный цикл JWT: получите токен, используйте его, дождитесь истечения, проверьте 401, обновите его
  3. Протестируйте RBAC: создайте фикстуры для ролей admin, editor и viewer; проверьте матрицу доступа
  4. Протестируйте IDOR: проверьте, что Пользователь А не может получить доступ к данным Пользователя Б
  5. Протестируйте утечку информации: проверьте, что сообщения об ошибках не раскрывают существование пользователя

Ключевые выводы

  • Тестируйте каждый метод аутентификации: валидные, отсутствующие, невалидные, просроченные и подделанные учётные данные
  • JWT-тесты должны покрывать истечение, подделку и потоки обновления
  • Тесты авторизации (RBAC) проверяют границы ролей и разрешений
  • IDOR-тесты проверяют, что пользователи не могут получить доступ к приватным данным других пользователей
  • Сообщения об ошибках не должны содержать утечек информации (перечисление пользователей, стек-трейсы)
  • Безопасность кук: проверяйте атрибуты HttpOnly, Secure и SameSite