You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 expired — epds_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.
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 #100 already touches security.feature for CSP scenarios — may be the right home.)
Symptom
A user visits
https://auth.certified.one/account/login, enters their email, and on submit lands on/account/send-otpshowing 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) returns403 {"error":"CSRF validation failed"}on a POST when either:epds_csrfcookie is not sent by the browser, orcsrfbody field is absent.Reproduced live on prod (
auth.certified.one, version0.6.3):So this is a client-side cookie-delivery / expiry condition, not a backend defect. Most likely triggers:
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.Secure; HttpOnly; SameSite=Laxcookie.epds_csrfcookie from an earlier scope (note the historicmagic_csrf→epds_csrfrename).Two problems to fix
/account/loginto mint a fresh token. Related to the existing "session expired dead-end" work (Re-submitting email after PAR expiry produces a "Session expired" dead-end #150, Browser back after expiry sends OTP via the default email template (client branding lost) #152).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.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 #100 already touches
security.featurefor 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)