feat: Implement CSRF protection and PostgreSQL support
- Added CSRF protection mechanism in the backend with utility functions for token management. - Introduced a new CSRF route to fetch the active CSRF token for SPA bootstrap flows. - Updated the auth routes to validate CSRF tokens on sensitive operations. - Configured PostgreSQL as a database option in the environment settings and Docker Compose. - Created a new SQLite configuration file for local development. - Enhanced the API client to automatically attach CSRF tokens to requests. - Updated various frontend components to utilize the new site origin utility for SEO purposes. - Modified Nginx configuration to improve redirection and SEO headers. - Added tests for CSRF token handling in the authentication routes.
This commit is contained in:
@@ -11,6 +11,37 @@ from app.services.ai_cost_service import init_ai_cost_db
|
||||
from app.services.site_assistant_service import init_site_assistant_db
|
||||
from app.services.contact_service import init_contact_db
|
||||
from app.services.stripe_service import init_stripe_db
|
||||
from flask.testing import FlaskClient
|
||||
from werkzeug.datastructures import Headers
|
||||
|
||||
|
||||
class CSRFTestClient(FlaskClient):
|
||||
"""Flask test client that auto-injects the SPA CSRF header for browser requests."""
|
||||
|
||||
def open(self, *args, **kwargs):
|
||||
path = args[0] if args and isinstance(args[0], str) else kwargs.get("path", "")
|
||||
method = str(kwargs.get("method", "GET")).upper()
|
||||
headers = Headers(kwargs.pop("headers", {}))
|
||||
|
||||
should_add_csrf = (
|
||||
method in {"POST", "PUT", "PATCH", "DELETE"}
|
||||
and path != "/api/stripe/webhook"
|
||||
and "X-API-Key" not in headers
|
||||
and "X-CSRF-Token" not in headers
|
||||
)
|
||||
|
||||
if should_add_csrf:
|
||||
token_cookie = self.get_cookie("csrf_token")
|
||||
if token_cookie is None:
|
||||
FlaskClient.open(self, "/api/auth/csrf", method="GET")
|
||||
token_cookie = self.get_cookie("csrf_token")
|
||||
if token_cookie is not None:
|
||||
headers["X-CSRF-Token"] = token_cookie.value
|
||||
|
||||
if headers:
|
||||
kwargs["headers"] = headers
|
||||
|
||||
return super().open(*args, **kwargs)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -26,6 +57,7 @@ def app():
|
||||
os.environ['OUTPUT_FOLDER'] = output_folder
|
||||
|
||||
app = create_app('testing')
|
||||
app.test_client_class = CSRFTestClient
|
||||
app.config.update({
|
||||
'TESTING': True,
|
||||
'UPLOAD_FOLDER': upload_folder,
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
|
||||
class TestAuthRoutes:
|
||||
def test_csrf_bootstrap_returns_token(self, client):
|
||||
response = client.get('/api/auth/csrf')
|
||||
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.get_json()['csrf_token'], str)
|
||||
assert response.get_json()['csrf_token']
|
||||
|
||||
def test_register_success(self, client):
|
||||
response = client.post(
|
||||
'/api/auth/register',
|
||||
@@ -77,3 +84,13 @@ class TestAuthRoutes:
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.get_json() == {'authenticated': False, 'user': None}
|
||||
|
||||
def test_register_rejects_invalid_csrf_token(self, client):
|
||||
response = client.post(
|
||||
'/api/auth/register',
|
||||
json={'email': 'csrf@example.com', 'password': 'secretpass123'},
|
||||
headers={'X-CSRF-Token': 'invalid-token'},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert 'csrf' in response.get_json()['error'].lower()
|
||||
|
||||
Reference in New Issue
Block a user