feat: implement 5 QA-driven security and quality proposals

Implemented proposals from comprehensive QA review:

1. extend-csrf-protection
   - Add POST to CSRF protected methods in frontend
   - Global CSRF middleware for all state-changing operations
   - Update tests with CSRF token fixtures

2. tighten-cors-websocket-security
   - Replace wildcard CORS with explicit method/header lists
   - Disable query parameter auth in production (code 4002)
   - Add per-user WebSocket connection limit (max 5, code 4005)

3. shorten-jwt-expiry
   - Reduce JWT expiry from 7 days to 60 minutes
   - Add refresh token support with 7-day expiry
   - Implement token rotation on refresh
   - Frontend auto-refresh when token near expiry (<5 min)

4. fix-frontend-quality
   - Add React.lazy() code splitting for all pages
   - Fix useCallback dependency arrays (Dashboard, Comments)
   - Add localStorage data validation in AuthContext
   - Complete i18n for AttachmentUpload component

5. enhance-backend-validation
   - Add SecurityAuditMiddleware for access denied logging
   - Add ErrorSanitizerMiddleware for production error messages
   - Protect /health/detailed with admin authentication
   - Add input length validation (comment 5000, desc 10000)

All 521 backend tests passing. Frontend builds successfully.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2026-01-12 23:19:05 +08:00
parent df50d5e7f8
commit 35c90fe76b
48 changed files with 2132 additions and 403 deletions

View File

@@ -1,5 +1,16 @@
import pytest
from app.core.security import create_access_token, decode_access_token, create_token_payload
from app.core.security import (
create_access_token,
decode_access_token,
create_token_payload,
generate_refresh_token,
store_refresh_token,
validate_refresh_token,
invalidate_refresh_token,
invalidate_all_user_refresh_tokens,
decode_refresh_token_user_id,
get_refresh_token_key,
)
class TestJWT:
@@ -59,7 +70,7 @@ class TestAuthEndpoints:
def test_get_me_without_auth(self, client):
"""Test accessing /me without authentication."""
response = client.get("/api/auth/me")
assert response.status_code == 403
assert response.status_code == 401 # 401 for unauthenticated, 403 for unauthorized
def test_get_me_with_auth(self, client, admin_token):
"""Test accessing /me with valid authentication."""
@@ -72,13 +83,196 @@ class TestAuthEndpoints:
assert data["email"] == "ymirliu@panjit.com.tw"
assert data["is_system_admin"] is True
def test_logout(self, client, admin_token, mock_redis):
def test_logout(self, client, auth_headers, mock_redis):
"""Test logout endpoint."""
response = client.post(
"/api/auth/logout",
headers={"Authorization": f"Bearer {admin_token}"},
headers=auth_headers,
)
assert response.status_code == 200
# Verify session is removed
assert mock_redis.get("session:00000000-0000-0000-0000-000000000001") is None
class TestRefreshToken:
"""Test refresh token functionality."""
def test_generate_refresh_token(self):
"""Test that refresh tokens are generated correctly."""
token = generate_refresh_token()
assert token is not None
assert isinstance(token, str)
assert len(token) > 20 # URL-safe base64 encoded 32 bytes
def test_generate_unique_refresh_tokens(self):
"""Test that each generated token is unique."""
tokens = [generate_refresh_token() for _ in range(100)]
assert len(set(tokens)) == 100 # All tokens should be unique
def test_store_and_validate_refresh_token(self, mock_redis):
"""Test storing and validating refresh tokens."""
user_id = "test-user-123"
token = generate_refresh_token()
# Store the token
store_refresh_token(mock_redis, user_id, token)
# Validate the token
assert validate_refresh_token(mock_redis, user_id, token) is True
# Wrong user should fail
assert validate_refresh_token(mock_redis, "wrong-user", token) is False
# Wrong token should fail
assert validate_refresh_token(mock_redis, user_id, "wrong-token") is False
def test_invalidate_refresh_token(self, mock_redis):
"""Test invalidating a refresh token."""
user_id = "test-user-123"
token = generate_refresh_token()
# Store and verify
store_refresh_token(mock_redis, user_id, token)
assert validate_refresh_token(mock_redis, user_id, token) is True
# Invalidate
result = invalidate_refresh_token(mock_redis, user_id, token)
assert result is True
# Should no longer be valid
assert validate_refresh_token(mock_redis, user_id, token) is False
def test_invalidate_all_user_refresh_tokens(self, mock_redis):
"""Test invalidating all refresh tokens for a user."""
user_id = "test-user-123"
tokens = [generate_refresh_token() for _ in range(3)]
# Store multiple tokens
for token in tokens:
store_refresh_token(mock_redis, user_id, token)
# Verify all are valid
for token in tokens:
assert validate_refresh_token(mock_redis, user_id, token) is True
# Invalidate all
count = invalidate_all_user_refresh_tokens(mock_redis, user_id)
assert count == 3
# All should be invalid now
for token in tokens:
assert validate_refresh_token(mock_redis, user_id, token) is False
def test_decode_refresh_token_user_id(self, mock_redis):
"""Test finding user ID from refresh token."""
user_id = "test-user-456"
token = generate_refresh_token()
# Store the token
store_refresh_token(mock_redis, user_id, token)
# Find user ID
found_user_id = decode_refresh_token_user_id(token, mock_redis)
assert found_user_id == user_id
# Invalid token should return None
assert decode_refresh_token_user_id("invalid-token", mock_redis) is None
class TestRefreshTokenEndpoint:
"""Test the refresh token API endpoint."""
def test_refresh_token_success(self, client, db, mock_redis):
"""Test successful token refresh."""
user_id = "00000000-0000-0000-0000-000000000001"
# Generate and store a refresh token
refresh_token = generate_refresh_token()
store_refresh_token(mock_redis, user_id, refresh_token)
# Call refresh endpoint
response = client.post(
"/api/auth/refresh",
json={"refresh_token": refresh_token},
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert "refresh_token" in data
assert data["token_type"] == "bearer"
assert data["expires_in"] > 0
# Old refresh token should be invalidated (rotation)
assert validate_refresh_token(mock_redis, user_id, refresh_token) is False
# New refresh token should be valid
assert validate_refresh_token(mock_redis, user_id, data["refresh_token"]) is True
def test_refresh_token_invalid(self, client, mock_redis):
"""Test refresh with invalid token."""
response = client.post(
"/api/auth/refresh",
json={"refresh_token": "invalid-token"},
)
assert response.status_code == 401
assert "Invalid or expired refresh token" in response.json()["detail"]
def test_refresh_token_rotation(self, client, db, mock_redis):
"""Test that refresh tokens are rotated (old one invalidated)."""
user_id = "00000000-0000-0000-0000-000000000001"
# Generate and store initial refresh token
initial_token = generate_refresh_token()
store_refresh_token(mock_redis, user_id, initial_token)
# First refresh
response1 = client.post(
"/api/auth/refresh",
json={"refresh_token": initial_token},
)
assert response1.status_code == 200
new_token = response1.json()["refresh_token"]
# Try to reuse the old token (should fail due to rotation)
response2 = client.post(
"/api/auth/refresh",
json={"refresh_token": initial_token},
)
assert response2.status_code == 401
# New token should still work
response3 = client.post(
"/api/auth/refresh",
json={"refresh_token": new_token},
)
assert response3.status_code == 200
def test_refresh_token_disabled_user(self, client, db, mock_redis):
"""Test that disabled users cannot refresh tokens."""
from app.models.user import User
# Create a disabled user
disabled_user = User(
id="disabled-user-123",
email="disabled@example.com",
name="Disabled User",
is_active=False,
)
db.add(disabled_user)
db.commit()
# Generate and store refresh token for disabled user
refresh_token = generate_refresh_token()
store_refresh_token(mock_redis, disabled_user.id, refresh_token)
# Try to refresh
response = client.post(
"/api/auth/refresh",
json={"refresh_token": refresh_token},
)
assert response.status_code == 403
assert "disabled" in response.json()["detail"].lower()