Add invisible reCAPTCHA verification to the signup and forgot-password forms - #1633
Conversation
…assword Mints a reCAPTCHA token (enterprise.js, score-based — no challenge is ever shown; the standard badge renders bottom-right) as each form submits and sends it as captchaToken in the POST /User and POST /ForgotPassword bodies, so central-manager can reject scripted signups and reset-email bombing before it does any work via the Enterprise assessments API. Tokens are single use and expire in ~2 minutes, so one is minted per submit — a retry after any failure gets a fresh token by construction, and a mint that hangs is bounded at 4s and degrades to a tokenless submit rather than a dead form. A 403 from central-manager shows an inline notice; when the script itself could not run (ad blocker), the notice says that instead of asking the user to retry something that cannot succeed, and the script load stays retryable rather than caching the failure. The site key comes from VITE_RECAPTCHA_SITE_KEY. Unset (all deployed environments today) runs no reCAPTCHA at all — the loader is tree-shaken out of the bundle — so nothing changes until a key is configured. Refs HarperFast/central-manager#627 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ication Review findings: a script element that fired neither load nor error left the cached load promise pending forever, so every later submit ate the mint timeout and could never recover without a reload — loads now time out at 8s, remove their node (including the loaded-without-API stub case), and clear the cache so a retry re-injects. The CAPTCHA 403 classifier also reads an RFC 9457 problem body's title, so a server-side response reshape cannot silently downgrade the ad-blocker guidance to the generic error path. Refs HarperFast/central-manager#627 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refs HarperFast/central-manager#627 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A low score cannot be retried away — the signals behind it do not change — so repeating "please try again" is a dead end. A second consecutive rejection appends the existing ContactUs link (mailto:support@harperdb.io), and any non-CAPTCHA failure resets the count. Refs HarperFast/central-manager#627 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refs HarperFast/central-manager#627 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Public by design — it ships in the bundle. Pairs with RECAPTCHA_SITE_KEY in FABRIC_PRODUCTION; the two must match or every assessment rejects. Refs HarperFast/central-manager#627 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Code Review
This pull request integrates Google reCAPTCHA v3 into the public Sign Up and Forgot Password forms to protect against bot submissions. It introduces environment configuration variables, a custom useCaptchaChallenge hook, and script-loading utilities with timeout mechanisms, alongside comprehensive unit tests. The review feedback highlights a potential unhandled promise rejection in the token minting timeout logic when mintToken rejects after the timeout has already resolved, suggesting a direct .catch attachment to resolve it.
|
ran locally, didn't notice any regression, seems ok -- captcha didn't show up for me, but i assume it assessed my login as low risk, and didn't trigger the display, which is a positive result. |
|
Thanks for running it — the no-regression signal on the surrounding flows is genuinely useful. One thing worth flagging so you don't credit it with more than it did: locally you were almost certainly not exercising reCAPTCHA at all. Also — and this is your own thread from the other comment — login isn't gated. Only signup and forgot-password are, so a sign-in would never have been assessed regardless. On "captcha didn't show up": it never will. Score-based reCAPTCHA Enterprise is fully invisible — there's no checkbox and no image challenge to present, ever. The only visible artifact is Google's badge bottom-right, and that only appears once a site key is configured. (Scoring runs 0.0 bot → 1.0 human, so a good visitor scores high.) If you want to actually drive it, point a local studio at stage, which is enforcing right now: The stage key allows |
|
Correction to my previous comment: stage is no longer enforcing — David rolled the central-manager branch back off stage after I wrote that. So the There's no environment enforcing right now, so there isn't a self-serve way to drive the full loop until central-manager#677 is deployed somewhere again. Setting the site key locally still shows you the client half — badge renders, The end-to-end results in the description were captured while stage was enforcing (signup 201 with a real token, forgot-password 200, 403 for garbage and for no token, plus the score branch firing when the threshold was raised), so that evidence stands — it just isn't reproducible on demand at the moment. — Claude (Fable 5) |
|
** i did a sign-up as my test, i mispoke |
|
Good to know — though it lands the same either way: with no |
|
I borrowed the one from stage, set it in my local .env.local, to exercise it. |
|
Ah — then you exercised more than I gave you credit for, and I was wrong twice over. Sorry for the noise. With the key set you'd have had the full client half: badge bottom-right, token minted at submit, |
kriszyp
left a comment
There was a problem hiding this comment.
Sounds like a good security improvement.
🤖 Reviewed with Codex
…CHA toast Review findings from @kriszyp: isCaptchaVerificationError returned the first STRING field rather than the first field naming the cause, so { error: 'Forbidden', message: 'Verification failed' } read as non-CAPTCHA and silently dropped the ad-blocker guidance and support path. Every candidate field is now checked and RFC 9457's detail is included. A CAPTCHA 403 also raised the global toast alongside the inline notice, because useForgotPasswordMutation never opted out. It now does, and non-CAPTCHA failures are re-raised through errorHandler so they keep the toast they have always had. The test's QueryClient had no mutationCache, so it could not have caught the duplication — it now wires the app's real mutationErrorHandler, as SignUp.test.tsx already did. Both new tests fail on the pre-fix code. Refs HarperFast/central-manager#627 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds an invisible reCAPTCHA Enterprise check to the two public auth forms — signup and forgot-password — so central-manager#677 can reject the scripted signups and reset-email bombing from the August 2026 campaign before it creates a user or sends an email. Nobody ever picks pictures: Google scores the interaction in the background, the form sends a single-use token as
captchaToken, and the only visible change is Google's standard badge on those two pages. WithVITE_RECAPTCHA_SITE_KEYunset the loader is tree-shaken out of the bundle entirely, so nothing changes in an environment that has no key.For the human reviewer
"verification failed"across two repos, rather than a structured error code central-manager owns. Cheap to change now, sticky once both sides ship: if CM ever rewords that message the client silently drops back to the generic error and the tailored guidance disappears. A shared error code would be the root-cause fix.ContactUsblock is duplicated in both forms rather than extracted to a shared component. Two copies can drift; themailtoitself can't, since it lives in the existingContactUs. Extraction is a ~6-line JSX component — worth it if you disagree with the call.Verification
pnpm test— 2298 pass, 11 skipped, 296 files; coverage for this change lives in four test files, three of them new (recaptchaScript.test.ts,isCaptchaVerificationError.test.ts,ForgotPassword.test.tsx) plus additions to the existingSignUp.test.tsx.tsc -b,oxlint,dprintclean. Rebased onto currentstage(23 commits, including the typed-Axios 1.19 adaptation) and re-run against a freshpnpm install --frozen-lockfile, not stalenode_modules.'signup'action, origin hostname and score all cleared a real Google assessment — and forgot-password 200, against 403 for a garbage token and 403 for no token at all. Raising theRECAPTCHA_MIN_SCOREConfiguration row then made a normally-passing token 403 with this PR's "please try again" wording, which is the score branch and the client's message selection both firing for real.mailto:support@harperdb.iolink. That last check caught a formatter-eaten space ("try again.Contact us"), now fixed and pinned by an assertion.Review coverage
Authored by Claude (Fable 5). Cross-model pre-push review, two rounds against
origin/stage. Round 1 (full, @ 7466e5c): codexgpt-5.6-sol✓ (graded), gemini viaagy(default model) ✓, Harper domain adjudication ✓ —Adjudicated-Severity: minor; cursor-grok ✗ (cursor-agent not installed on this machine). Round 2 (delta, @ f5e4b9f): same three legs, no new findings — converged. Independent coverage: true, both rounds. The comment nit was taken; the double-message finding is deliberately unresolved and is ledger entry 1 above. Three commits since the last reviewed SHA are trivial-tier and unreviewed by the legs: two comment rewordings and the prod site-key value (a config value flowing through the already-reviewed configured/unconfigured branches).Refs HarperFast/central-manager#627 — cross-repo and based on
stage, so closing keywords won't fire; #627 closes when both halves land.stage.studio.harperfabric.comis currently 403ing both auth forms until this deploys — this PR is the fix. For production the reverse holds: this must deploy beforeRECAPTCHA_API_KEYlands inFABRIC_PRODUCTION, or prod signups 403 in the gap. Note the badge appears on prod as soon as this deploys, ahead of enforcement.🤖 Generated with Claude Code
Human-Review-Need: 4 @ f53d9c6