Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 11 additions & 39 deletions frontend/src/utils/csrf.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,60 +9,32 @@
*/

/**
* Gets the CSRF token from the page's meta tag, with fallback to legacy API endpoint.
* Gets the CSRF token from the page's meta tag.
*
* Preferred method: The backend sets a meta tag like:
* The backend sets a meta tag like:
* <meta name="csrf-token" content="TOKEN_VALUE">
*
* Fallback (DEPRECATED): Fetches token from /api/generate-csrf-token endpoint.
* This fallback exists for backward compatibility but will be removed in a future version.
*
* This token must be included in the X-CSRFToken header for all
* state-changing requests (POST, PUT, PATCH, DELETE).
*
* @returns {Promise<string>} The CSRF token
* @throws {Error} If CSRF token cannot be obtained from either source
* @returns {string} The CSRF token
* @throws {Error} If the CSRF token meta tag is missing or empty
*
* @example
* const csrfToken = await getCsrfToken();
* const csrfToken = getCsrfToken();
* axios.post('/api/suggest-new-point', data, {
* headers: { 'X-CSRFToken': csrfToken }
* });
*/
export const getCsrfToken = async () => {
export const getCsrfToken = () => {
const metaTag = document.querySelector('meta[name="csrf-token"]');
const token = metaTag?.getAttribute('content');

// Try to get token from meta tag first (preferred method)
if (metaTag) {
const token = metaTag.getAttribute('content');
if (token) {
return token;
}
}

// Fallback to legacy API endpoint (DEPRECATED)
console.warn(
'⚠️ DEPRECATION WARNING: CSRF token meta tag not found. ' +
'Falling back to /api/generate-csrf-token endpoint. ' +
'This fallback is DEPRECATED and will be removed in a future version. ' +
'Please ensure the backend includes <meta name="csrf-token" content="..."> in the page HTML.',
);

try {
const response = await fetch('/api/generate-csrf-token');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (!data.csrf_token) {
throw new Error('API response missing csrf_token field');
}
return data.csrf_token;
} catch (error) {
console.error('Failed to fetch CSRF token from legacy endpoint:', error);
if (!token) {
throw new Error(
'CSRF token not found. Neither meta tag nor /api/generate-csrf-token endpoint provided a valid token.',
'CSRF token not found. Ensure the backend includes <meta name="csrf-token" content="..."> in the page HTML.',
);
}
};

return token;
};
6 changes: 0 additions & 6 deletions goodmap/api_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,6 @@ class VersionResponse(BaseModel):
backend: str = Field(..., description="Backend version")


class CSRFTokenResponse(BaseModel):
"""Response model for CSRF token endpoint (deprecated)."""

csrf_token: str = Field(..., description="CSRF token")


class PaginationParams(BaseModel):
"""Common pagination and filtering parameters."""

Expand Down
18 changes: 0 additions & 18 deletions goodmap/core_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from werkzeug.exceptions import HTTPException

from goodmap.api_models import (
CSRFTokenResponse,
ErrorResponse,
LocationReportRequest,
LocationReportResponse,
Expand Down Expand Up @@ -113,7 +112,6 @@ def core_pages(
database,
languages: LanguagesMapping,
notifier_function,
csrf_generator,
location_model,
photo_attachment_config: AttachmentConfig,
feature_flags: FeatureFlagSet,
Expand Down Expand Up @@ -360,22 +358,6 @@ def get_version():
version_info = {"backend": importlib.metadata.version("goodmap")}
return jsonify(version_info)

@core_api_blueprint.route("/generate-csrf-token", methods=["GET"])
@spec.validate(resp=Response(HTTP_200=CSRFTokenResponse))
@deprecation.deprecated(
deprecated_in="1.1.8",
details="This endpoint for explicit CSRF token generation is deprecated. "
"CSRF protection remains active in the application.",
)
def generate_csrf_token():
"""Generate CSRF token (DEPRECATED).

This endpoint is deprecated and maintained only for backward compatibility.
CSRF protection remains active in the application.
"""
csrf_token = csrf_generator()
return {"csrf_token": csrf_token}

@core_api_blueprint.route("/categories", methods=["GET"])
@spec.validate()
def get_categories():
Expand Down
3 changes: 1 addition & 2 deletions goodmap/goodmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from flask import Blueprint, redirect, render_template, session
from flask_babel import gettext
from flask_wtf.csrf import CSRFProtect, generate_csrf
from flask_wtf.csrf import CSRFProtect
from platzky import platzky
from platzky.config import languages_dict
from platzky.models import CmsModule
Expand Down Expand Up @@ -253,7 +253,6 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
app.db,
languages_dict(config.languages),
app.notify,
generate_csrf,
location_model,
photo_attachment_config=photo_attachment_config,
feature_flags=config.feature_flags,
Expand Down
6 changes: 0 additions & 6 deletions tests/unit_tests/test_core_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,6 @@ def test_version_endpoint_returns_version(mock_returning_version, test_app):
assert response.json == {"backend": "0.1.2"}


def test_csrf_token_endpoint_returns_token(test_app):
response = test_app.get("/api/generate-csrf-token")
assert response.status_code == 200
assert "csrf_token" in response.json


def test_api_doc_index(test_app):
response = test_app.get("/api/doc")
assert response.status_code == 200
Expand Down
Loading