Logout now requires Authorization: Bearer <access-token>.
This commit is contained in:
@@ -275,6 +275,13 @@ curl -X POST http://localhost:8000/api/auth/refresh \\
|
||||
|
||||
Refresh-token reuse or a mismatched device ID returns `401` and revokes the token family. Signing out revokes the token family, while changing the password or completing a password reset revokes all sessions for the user.
|
||||
|
||||
Sign out with the access token in the bearer header:
|
||||
|
||||
```sh
|
||||
curl -X POST http://localhost:8000/api/auth/logout \\
|
||||
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
|
||||
```
|
||||
|
||||
Submit a link using the returned access token:
|
||||
|
||||
```sh
|
||||
|
||||
+6
-4
@@ -42,13 +42,15 @@ The application should remain behind the production reverse proxy, with real DNS
|
||||
|
||||
### SA-001: Logout uses non-standard token transport
|
||||
|
||||
**Severity:** High
|
||||
**Evidence:** `POST /api/auth/logout` accepts `{"token": ...}` in the JSON request body, and the web frontend sends the access token this way.
|
||||
**Severity:** High, remediated in current worktree
|
||||
**Evidence before remediation:** `POST /api/auth/logout` accepted `{"token": ...}` in the JSON request body, and the web frontend sent the access token this way.
|
||||
**Impact:** Request bodies may be captured by debugging middleware, application logs, or monitoring systems. The endpoint also diverges from the bearer-header contract used elsewhere, increasing the chance of inconsistent token handling.
|
||||
|
||||
**Recommendation:** Make logout use `Authorization: Bearer <access-token>` and revoke the authenticated token or its family server-side. If a compatibility period is required, support both forms temporarily, prefer the header, and remove the body form after client migration. Add tests proving body-only tokens are rejected once compatibility is removed.
|
||||
**Current state:** Logout requires `Authorization: Bearer <access-token>`, rejects body-only tokens with `401`, and revokes the token family server-side. The web frontend and Firefox extension send the header; regression coverage verifies access and refresh tokens are invalid after logout.
|
||||
|
||||
**Priority:** High.
|
||||
**Recommendation:** Keep logout header-only, retain family revocation, avoid logging authorization headers, and rotate legacy sessions issued before this change.
|
||||
|
||||
**Priority:** Completed in code; legacy session rotation and log hygiene remain.
|
||||
|
||||
### SA-002: Raw infrastructure errors are returned to clients
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Chat Log
|
||||
|
||||
### User
|
||||
Remedy SA-001: Logout uses non-standard token transport.
|
||||
|
||||
### Assistant outcome
|
||||
Migrated logout to require `Authorization: Bearer <access-token>`, updated the web frontend and Firefox extension, and added regression coverage proving body-only logout is rejected while header logout revokes the access token and refresh-token family. Updated SA-001, the checklist, and README examples.
|
||||
|
||||
### User
|
||||
Perform a new security audit overwriting Security-audit.md with new and remaining issues.
|
||||
|
||||
|
||||
@@ -200,6 +200,7 @@
|
||||
194. Replace LINKLOG_TOKEN_EXPIRY_DAYS with LINKLOG_TOKEN_EXPIRY_MINUTES, add LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS to production Compose, and add a CI configuration consistency test.
|
||||
195. Perform a new security audit overwriting Security-audit.md with new and remaining issues.
|
||||
195. Update SA-012 and README for the implemented refresh-token lifecycle, revocation behavior, and refresh endpoint.
|
||||
196. Remedy SA-001: migrate logout from JSON token transport to the Authorization bearer header.
|
||||
|
||||
## Future entries
|
||||
|
||||
|
||||
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.app.api.dependencies import get_current_user
|
||||
@@ -119,10 +119,12 @@ def reset_password_endpoint(payload: PasswordResetRequest):
|
||||
|
||||
|
||||
@router.post('/logout')
|
||||
def logout(payload: dict):
|
||||
token = payload.get('token')
|
||||
def logout(authorization: str | None = Header(default=None)):
|
||||
if not authorization or not authorization.startswith('Bearer '):
|
||||
raise HTTPException(status_code=401, detail='Missing or invalid Authorization header')
|
||||
token = authorization.replace('Bearer ', '', 1).strip()
|
||||
if not token:
|
||||
raise HTTPException(status_code=400, detail='Token is required')
|
||||
raise HTTPException(status_code=401, detail='Missing or invalid Authorization header')
|
||||
revoked = revoke_token(token)
|
||||
if not revoked:
|
||||
raise HTTPException(status_code=404, detail='Token not found or already revoked')
|
||||
|
||||
@@ -55,6 +55,17 @@ def test_login_returns_token():
|
||||
assert client.get('/api/auth/me', params={'token': payload['access_token']}).status_code == 401
|
||||
|
||||
|
||||
def test_logout_requires_bearer_header_and_revokes_token_family():
|
||||
login = client.post('/api/auth/login', json={'email': 'alice@example.com', 'password': 'secret123'}).json()
|
||||
token = login['access_token']
|
||||
headers = {'Authorization': f'Bearer {token}'}
|
||||
assert client.post('/api/auth/logout', json={'token': token}).status_code == 401
|
||||
assert client.get('/api/auth/me', headers=headers).status_code == 200
|
||||
assert client.post('/api/auth/logout', headers=headers).status_code == 200
|
||||
assert client.get('/api/auth/me', headers=headers).status_code == 401
|
||||
assert client.post('/api/auth/refresh', json={'refresh_token': login['refresh_token'], 'device_id': login['device_id']}).status_code == 401
|
||||
|
||||
|
||||
def test_login_rate_limit_locks_out_after_five_failures_and_resets_on_success():
|
||||
email = f'unknown-{uuid4().hex}@example.com'
|
||||
for attempt in range(5):
|
||||
@@ -643,8 +654,7 @@ def test_only_link_owner_can_edit_link():
|
||||
|
||||
def test_logout_revokes_token_and_admin_can_list_plugins():
|
||||
headers = login_headers()
|
||||
token = headers['Authorization'].removeprefix('Bearer ')
|
||||
assert client.post('/api/auth/logout', json={'token': token}).status_code == 200
|
||||
assert client.post('/api/auth/logout', headers=headers).status_code == 200
|
||||
|
||||
revoked_response = client.post('/api/links', headers=headers, json={
|
||||
'title': 'Should fail',
|
||||
|
||||
@@ -12,8 +12,7 @@ logoutButton.addEventListener('click', async () => {
|
||||
if (token) {
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({token}),
|
||||
headers: {Authorization: `Bearer ${token}`},
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
|
||||
|
||||
@@ -186,8 +186,7 @@ async function signOut() {
|
||||
if (sessionSettings.accessToken && settings.backendUrl && await hasBackendPermission(backendUrl)) {
|
||||
await fetch(`${backendUrl}/api/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: sessionSettings.accessToken }),
|
||||
headers: {Authorization: `Bearer ${sessionSettings.accessToken}`},
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
await clearSession();
|
||||
|
||||
Reference in New Issue
Block a user