fix: CSRF handshake and Referer header for Django 4+ HTTPS session auth#125
fix: CSRF handshake and Referer header for Django 4+ HTTPS session auth#125blarghmatey wants to merge 2 commits into
Conversation
DRF-based xqueue deployments (e.g. edx-submissions) return 401/403 when a session is missing or expired rather than issuing a redirect to the login page. Previously the watcher would treat these as fatal errors and stop processing. Changes: - Extend the auth-failure branch to cover 401 and 403 in addition to the existing 301/302 redirect handling. - Add a `reauthenticated` guard so a persistent auth failure (e.g. wrong credentials) causes one clean error rather than a login storm. - Include the response body in the unexpected-status error message to aid debugging. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
xqueue-watcher authenticates via session cookies against a DRF-backed xqueue endpoint. Two failures caused persistent 403s in production: 1. No CSRF token on put_result POST xqueue-watcher's _login() POSTed credentials without first obtaining a CSRF cookie, so the session CSRF token was never set. Subsequent put_result POSTs arrived with no X-CSRFToken header and DRF's SessionAuthentication.enforce_csrf() rejected them with 403. Fix: GET /xqueue/login/ before POSTing credentials. edx-submissions now exposes GET on this endpoint for exactly this purpose (openedx/edx-submissions#352). Older deployments that only accept POST return 405; we log and proceed since the login POST is AllowAny and therefore CSRF-exempt. After login, persist the CSRF token in the session headers so every subsequent mutating request automatically includes X-CSRFToken. Also clear any stale session cookies before the GET so the request arrives as an anonymous user and receives a fresh CSRF cookie. 2. Missing Referer header on HTTPS POST Django 4+ enforces an additional check on HTTPS POST requests: if the request has neither an Origin nor a Referer header, it returns 403 (REASON_NO_REFERER) even when the CSRF token itself is correct. requests.Session does not set Referer automatically. Fix: after a successful login, store Referer: <xqueue_server> in the session headers. All subsequent POSTs to the same server then pass Django's _check_referer() without any per-call plumbing. Both fixes are validated by new unit tests covering the full CSRF handshake, the Referer persistence, and the 403→reauth→retry cycle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Thanks for the pull request, @blarghmatey! This repository is currently unmaintained. 🔘 Find a technical reviewerTo get help with finding a technical reviewer, reach out to the community contributions project manager for this PR:
Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
Summary
xqueue-watcher authenticates against a DRF-backed xqueue endpoint via session
cookies. Two bugs in
_login()caused persistent 403 responses forput_resultin production after upgrading to Django 4+/5+:1. No CSRF token on
put_resultPOST_login()posted credentials directly without first obtaining a CSRF cookie.The session therefore had no
X-CSRFTokenheader, and DRF'sSessionAuthentication.enforce_csrf()rejected every subsequentput_resultPOST with 403.
Fix: GET
/xqueue/login/before posting credentials.openedx/edx-submissions#352
exposes a GET handler on the login endpoint for this purpose — it calls
get_token(request)which ensures Django sets thecsrftokencookie in theresponse. After login, the CSRF token is persisted in
session.headerssoevery subsequent mutating request automatically includes
X-CSRFToken.Older deployments that only accept POST will return 405; we log and proceed
since the login POST is
AllowAnyand therefore CSRF-exempt.Stale session cookies are cleared before the GET so the request arrives as an
anonymous user and receives a fresh cookie.
2. Missing
Refererheader on HTTPS POST (Django 4+ regression)Django 4+ adds an extra check in
CsrfViewMiddleware.process_view(): forHTTPS POST requests with no
HTTP_ORIGINheader it calls_check_referer()which raises
REASON_NO_REFERERifHTTP_REFERERis absent — before theCSRF token is even checked.
requests.Sessiondoes not addRefererautomatically, so all
put_resultPOSTs failed this check even when the CSRFtoken was correct.
Fix: after a successful login, store
Referer: <xqueue_server>in thesession headers. All subsequent POSTs to the same server pass Django's
_check_referer()without any per-call plumbing.The two fixes together fully resolve the production failure mode:
Also in this PR
_request()now treats 401 and 403 as authentication failures andre-authenticates, matching the behaviour for 301/302 redirects. DRF returns
these directly rather than redirecting to the login page.
reauthenticatedguard prevents infinite login storms on persistentfailures.
persistence, and the 403→reauth→retry cycle. All 21 tests pass.
Related
cookie can be obtained before the POST (server-side counterpart to this fix)
Test plan
python -m pytest tests/test_xqueue_client.py -vput_resultno longer returns 403🤖 Generated with Claude Code