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:
@@ -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 |
|
||||
|---|---|
|
||||
|
||||
Reference in New Issue
Block a user