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>
2.3 KiB
TASK-033 — Session token returned in JSON body alongside HttpOnly cookie
Severity: Medium
Where found
backend/app/routers/auth.py — login() returns LoginResponse(token=signed_token, expires_at=expires_at) in the JSON body and sets the HttpOnly cookie. backend/app/models/auth.py — LoginResponse.token field.
Why this is needed
The LoginResponse JSON body contains the full signed session token. JavaScript running on the page (including third-party analytics scripts or a future XSS injection) can read the response body from a fetch() call and store the token in localStorage or a non-HttpOnly cookie. The Bearer-header authentication path (Authorization: Bearer <token>) then allows using that extracted token, completely bypassing the protections provided by the HttpOnly cookie.
Goal
Prevent the session token from being accessible to JavaScript when using cookie-based authentication.
What to do
- For browser SPA consumers: Remove the
tokenfield fromLoginResponse. The HttpOnly cookie is the only token the browser needs. - If an API-first (non-browser) token flow is required, create a separate endpoint
POST /api/auth/tokenthat returns a token in the body and does not set a cookie. Document this endpoint as "for programmatic API clients only, not for browser use". - Update the frontend — verify that
AuthProviderdoes not useresponse.token(confirmed: it currently does not).
Possible traps and issues
- Any existing API client that relies on the token in the
LoginResponsebody will break. Check tests. - The
expires_atfield inLoginResponseis useful for the frontend to know when to prompt for re-login — this can remain. - The Bearer-token path in
require_auth(Authorization: Bearer) remains functional for programmatic clients using the dedicated token endpoint.
Docs changes needed
Features.md— document the authentication flow (cookie for browser, token endpoint for API clients).Backend-Development.md— authentication endpoint design.Web-Development.md— document that the frontend uses only the HttpOnly cookie.
Doc references
- Features.md — authentication
- Backend-Development.md — auth router design
- Web-Development.md — AuthProvider