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:
2026-04-26 19:38:33 +02:00
parent e2560f5db0
commit 93021500c3
7 changed files with 93 additions and 58 deletions

View File

@@ -1093,9 +1093,58 @@ The login endpoint (`POST /api/auth/login`) is protected against brute-force att
- IP extraction: `app.utils.client_ip.get_client_ip()`
- Dependency: `LoginRateLimiterDep` in `app.dependencies`
---
## 12. Password Hashing
## 12. Authentication Endpoints
#### Browser SPA (Cookie-Based)
The **primary** authentication flow for the frontend is **cookie-based** and protects the session token from JavaScript access:
1. **Login (`POST /api/auth/login`)**
- Accepts `LoginRequest` (password field)
- Returns `LoginResponse` containing **only** `expires_at` (ISO 8601 UTC timestamp)
- **Crucially:** The session token is **not** included in the JSON response body
- Instead, the token is set as an **HttpOnly** `SameSite=Lax` cookie named `bangui_session`
- Frontend automatically includes this cookie in all requests via `credentials: "include"`
2. **Why not return token in response body?**
- Third-party JavaScript (analytics, ads, XSS injections) can intercept `fetch()` response bodies
- If the token were in the response, malicious code could extract and store it in `localStorage`
- An attacker could then use it via the `Authorization: Bearer <token>` header, bypassing the HttpOnly cookie protection
- By returning **only** the expiry timestamp, we ensure the token stays exclusively in the HttpOnly cookie
3. **Session Validation (`GET /api/auth/session`)**
- Frontend calls this on app mount to verify the session is still valid on the server
- Works with both cookie and Bearer token authentication
- Returns `{"valid": true}` if the session exists and is not expired
- Returns **401 Unauthorized** if the session is invalid or expired
4. **Logout (`POST /api/auth/logout`)**
- Revokes the session in the database
- Clears the `bangui_session` cookie via `Set-Cookie` header
- Works with both cookie and Bearer token authentication
- Idempotent — calling without a session returns 200 without error
#### Programmatic API Clients (Bearer Token)
For non-browser clients (CLI tools, batch scripts, automation) that cannot use cookies, use the **Bearer token authentication path** by sending:
```http
Authorization: Bearer <token>
```
The token can be obtained by parsing the cookie from a login response or, in a future implementation, via a dedicated `POST /api/auth/token` endpoint (currently, these clients extract the token from cookies or use Bearer directly from the signed token value).
**Note:** Bearer token authentication is not recommended for browser-based clients because:
- Tokens must be stored somewhere (localStorage, sessionStorage, or request body)
- All storage mechanisms are accessible to JavaScript and thus vulnerable to XSS
- HttpOnly cookies provide better protection
---
## 13. Password Hashing
The master password is hashed using **bcrypt** with an auto-generated salt. All password validation uses the models in `app.models.auth` and `app.models.setup`.
@@ -1127,7 +1176,7 @@ The master password is hashed using **bcrypt** with an auto-generated salt. All
---
## 14. File I/O Conventions
## 15. File I/O Conventions
All file write operations to critical configuration files must be **atomic** to prevent corruption if the process is killed mid-write.
@@ -1205,7 +1254,7 @@ atomic_write(path, updated_content) # Atomic write, auto-cleanup on error
---
## 15. Git & Workflow
## 16. Git & Workflow
- **Branch naming:** `feature/<short-description>`, `fix/<short-description>`, `chore/<short-description>`.
- **Commit messages:** imperative tense, max 72 chars first line (`Add jail reload endpoint`, `Fix ban history query`).
@@ -1215,7 +1264,7 @@ atomic_write(path, updated_content) # Atomic write, auto-cleanup on error
---
## 16. Coding Principles
## 17. Coding Principles
These principles are **non-negotiable**. Every backend contributor must internalise and apply them daily.
@@ -1602,7 +1651,7 @@ When user-supplied URLs are fetched by the backend, validate them before making
---
## 17. Quick Reference — Do / Don't
## 18. Quick Reference — Do / Don't
| Do | Don't |
|---|---|