QA Engineer Skills 2026QA-2026Authentication Testing

Authentication Testing

Authentication is the gatekeeper of your API. Testing it thoroughly means verifying not just that valid credentials work, but that invalid credentials fail correctly, tokens expire as expected, and sensitive information is never leaked in error responses.


Authentication Methods

Method How It Works Common In
API Key Static key in header or query param Third-party integrations, internal APIs
OAuth 2.0 Token exchange flow (authorization code, client credentials) SaaS platforms, social login
JWT Self-contained signed token with claims Modern APIs, microservices
Session Cookie Server-side session with cookie ID Traditional web apps
Basic Auth Base64-encoded username:password in header Legacy APIs, simple internal tools

Testing Each Method

API Key Testing

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 Testing

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

Token Refresh Flow

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

Session Cookie Testing

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

Authorization Testing (RBAC)

Authentication verifies identity. Authorization verifies permissions. Test that users can only access what their role allows.

@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}"

Horizontal Authorization (IDOR)

Users should not access other users' data:

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

Security Checks

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

Practical Exercise

  1. Write tests for a login flow: valid credentials, invalid password, invalid email, empty fields
  2. Test JWT lifecycle: obtain token, use it, let it expire, verify 401, refresh it
  3. Test RBAC: create fixtures for admin, editor, and viewer roles; verify access matrix
  4. Test for IDOR: verify User A cannot access User B's data
  5. Test for information leakage: verify error messages do not reveal user existence

Key Takeaways

  • Test every auth method: valid, missing, invalid, expired, and tampered credentials
  • JWT tests should cover expiration, tampering, and refresh flows
  • Authorization (RBAC) tests verify role-permission boundaries
  • IDOR tests verify users cannot access other users' private data
  • Error messages should not leak information (user enumeration, stack traces)
  • Cookie security: verify HttpOnly, Secure, and SameSite attributes