TASK-033: Remove session token from JSON response body
Fixes a critical security vulnerability where the session token was being returned in the JSON response body of POST /api/auth/login. This exposed the token to JavaScript, allowing malicious scripts to steal it and bypass the HttpOnly cookie protection. Changes: - Backend: Remove 'token' field from LoginResponse model (auth.py) - Backend: Update login() endpoint to return only 'expires_at' - Frontend: Update LoginResponse type to exclude 'token' field - Backend: Update test helper _login() to extract token from cookie - Backend: Update test cases to verify token is NOT in response body - Documentation: Add section 'Authentication Endpoints' in Backend-Development.md - Documentation: Update Web-Development.md to explain HttpOnly cookie benefits Security benefit: Session tokens are now only accessible via HttpOnly cookies, protected from JavaScript access, XSS attacks, and malicious third-party scripts. The frontend continues to use only the cookie for authentication. All auth tests pass (23 tests). Type checking and linting pass with zero errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -30,10 +30,16 @@ async def _do_setup(client: AsyncClient) -> None:
|
||||
|
||||
|
||||
async def _login(client: AsyncClient, password: str = "Mysecretpass1!") -> str:
|
||||
"""Helper: perform login and return the session token."""
|
||||
"""Helper: perform login and return the session token from the cookie.
|
||||
|
||||
Note: The token is returned in the HttpOnly cookie, not in the JSON body.
|
||||
For testing Bearer token auth, we extract it from the cookie.
|
||||
"""
|
||||
resp = await client.post("/api/auth/login", json={"password": password})
|
||||
assert resp.status_code == 200
|
||||
return str(resp.json()["token"])
|
||||
token = resp.cookies.get(SESSION_COOKIE_NAME)
|
||||
assert token is not None
|
||||
return str(token)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -47,16 +53,15 @@ class TestLogin:
|
||||
async def test_login_succeeds_with_correct_password(
|
||||
self, client: AsyncClient
|
||||
) -> None:
|
||||
"""Login returns 200 and a session token for the correct password."""
|
||||
"""Login returns 200 and sets a session cookie for the correct password."""
|
||||
await _do_setup(client)
|
||||
response = await client.post(
|
||||
"/api/auth/login", json={"password": "Mysecretpass1!"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert "token" in body
|
||||
assert len(body["token"]) > 0
|
||||
assert "." in body["token"]
|
||||
# Token is not returned in the JSON body; it's set as an HttpOnly cookie
|
||||
assert "token" not in body
|
||||
assert "expires_at" in body
|
||||
|
||||
async def test_login_sets_cookie(self, client: AsyncClient) -> None:
|
||||
|
||||
Reference in New Issue
Block a user