hypercerts-org / hypercerts-org/ePDS
Sign-in shows raw {"error":"CSRF validation failed"} when epds_csrf cookie is absent at POST
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 8
- Forks
- 4
- Avg merge
- 5d 11h
- Merged PRs (30d)
- 3
Description
Symptom
A user visits https://auth.certified.one/account/login, enters their email, and on submit lands on /account/send-otp showing a raw JSON error page:
{"error": "CSRF validation failed"}
No styled page, no recovery path — a dead end.
Root cause
The auth-service is behaving correctly server-side. csrfProtection (packages/auth-service/src/middleware/csrf.ts) returns 403 {"error":"CSRF validation failed"} on a POST when either:
- the
epds_csrfcookie is not sent by the browser, or - the
csrfbody field is absent.
Reproduced live on prod (auth.certified.one, version 0.6.3):
# GET sets the cookie correctly:
set-cookie: epds_csrf=…; Max-Age=1800; Path=/; HttpOnly; Secure; SameSite=Lax
# POST WITHOUT the cookie → 403:
$ curl -d 'csrf=<token>&email=test@example.com' .../account/send-otp
403 {"error":"CSRF validation failed"}
# POST WITH the cookie → 200 (renders OTP form):
200 <!DOCTYPE html> …
So this is a client-side cookie-delivery / expiry condition, not a backend defect. Most likely triggers:
- Cookie expired —
epds_csrfhasMax-Age=1800(30 min). Load the login page, wait >30 min, then submit → cookie gone, but the page still shows a form with a now-orphaned token → 403. - Browser privacy settings / extensions blocking the
Secure; HttpOnly; SameSite=Laxcookie. - A stale or duplicate
epds_csrfcookie from an earlier scope (note the historicmagic_csrf→epds_csrfrename).
Two problems to fix
- UX: a CSRF failure should not surface raw JSON. It should render a styled "Your session timed out — reload and try again" page (consistent with the existing PAR-expiry / session-expired error pages), ideally auto-reloading
/account/loginto mint a fresh token. Related to the existing "session expired dead-end" work (hypercerts-org/ePDS#150, hypercerts-org/ePDS#152). - Robustness: consider whether the 30-minute
epds_csrflifetime is too short for the email-entry step, and/or refresh the token on render so a stale form can't outlive its cookie.
Test gap
This class of failure is not covered:
packages/auth-service/src/__tests__/csrf.test.tsmocksreq.cookiesdirectly — never exercises a real GET→POST cookie roundtrip, so it can't catch cookie-not-delivered.- The CSRF e2e scenarios in
features/security.feature("Forms include CSRF protection", "POST without CSRF token is rejected") are tagged@pendingand have no step definitions —e2e/cucumber.mjsexcludesnot @pending, so they never run.
Implementing those two scenarios (real cookie roundtrip + missing-cookie → 403 + friendly page) would close the gap. (PR hypercerts-org/ePDS#100 already touches security.feature for CSP scenarios — may be the right home.)
Affected code
packages/auth-service/src/middleware/csrf.tspackages/auth-service/src/routes/account-login.tsfeatures/security.feature(@pendingCSRF scenarios)
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with packages/auth-service/src/routes/account-login.ts and packages/auth-service/src/middleware/csrf.ts, then inspect the pending CSRF scenarios in features/security.feature and their handling in e2e/cucumber.mjs. The work is done when a missing-cookie POST follows the friendly recovery flow instead of showing raw JSON, and real GET-to-POST cookie-roundtrip scenarios run successfully.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- authentication, backend, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100