hypercerts-org / hypercerts-org/ePDS

Sign-in shows raw {"error":"CSRF validation failed"} when epds_csrf cookie is absent at POST

Open
#179 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug Product: ePDS
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_csrf cookie is not sent by the browser, or
  • the csrf body 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 expiredepds_csrf has Max-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=Lax cookie.
  • A stale or duplicate epds_csrf cookie from an earlier scope (note the historic magic_csrfepds_csrf rename).

Two problems to fix

  1. 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/login to mint a fresh token. Related to the existing "session expired dead-end" work (hypercerts-org/ePDS#150, hypercerts-org/ePDS#152).
  2. Robustness: consider whether the 30-minute epds_csrf lifetime 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.ts mocks req.cookies directly — 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 @pending and have no step definitions — e2e/cucumber.mjs excludes not @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.ts
  • packages/auth-service/src/routes/account-login.ts
  • features/security.feature (@pending CSRF scenarios)

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.