Repository navigation
Ask unverified accounts for the email code before Approve - #1566
Merged
Merged
Conversation
On Cloud the MCP consent page accepted the password of an account whose address was never verified and completed the authorization. The client then held a token that /mcp refuses on every call, and retried it for weeks. The page now asks for the 6-digit code first (with a resend and the social sign-in buttons) and only then continues to Approve. The code logic moves from AuthController to EmailVerificationService so the dashboard and the consent page share it. Self-hosted is unchanged.
The step's buttons picked what ran through a posted action field, which CodeQL flags as a user-controlled bypass of a sensitive call. Resend, continue and social now post to their own routes via formaction, each checking the CSRF token, the pending sign-in and the account through one helper.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Cloud, the MCP consent page (
/auth/login) completed the authorization for a password account whose email address was never verified. The AI client then got a token that/mcprefuses on every call (EmailVerifiedGuard) and kept retrying it for weeks: 36 accounts since 28 Sep, 11 still retrying on 6 Nov (about 432 refused calls in seven hours). Details in #1565.The consent page now asks such an account for its 6-digit code before Approve, then continues exactly as a verified sign-in would.
Changes
LoginController: on Cloud, a correct password for an unverified account renders a code step instead of settinglogin_user, so/callbackcannot issue a code. The step has the code field (Enter submits), Send a new code, I opened the link in the email (continues once the link verified the address), and the enabled Continue with Google/Microsoft/GitHub buttons (they verify the address; for an unverified account they also drop the password, as social sign-in already does).POST /auth/verify-email(the code),POST /auth/verify-email/resend,POST /auth/verify-email/continueandPOST /auth/verify-email/social/:provider. The buttons reach them throughformaction, so no posted field picks what runs; each route goes through one helper that checks the CSRF token, the pending sign-in cookie and the account. The pending account id is in a signed, httpOnlypending_verifycookie (15 min, the code lifetime), with double-submit CSRF throughverify_csrf. The account is re-read on every post; an account made SSO-only meanwhile starts over.resend-verification.EmailVerificationServicewith the logic moved out ofAuthController(sendCode,verifyCode,markVerifiedwith the Cloud trial and the ChatGPT Ads report,codesSentWithin). The dashboard endpoints and the consent page use the same code path.AuthControllerbuilds the service from its own dependencies, so its constructor and its specs stay as they are. The code is now drawn withcrypto.randomIntinstead ofMath.random.startSocialSignInso the code step can reuse it.docs/integrations/claude.mdexplains the code step;llms-full.txtregenerated.Not changed: the dashboard session ("Allow access as …") already refuses unverified Cloud accounts (
resolveDashboardSessionUser), and social sign-in only accepts provider-verified addresses. Accounts that already hold a token are still refused on/mcp, with the existing notice.Known gap: the browser-side Google Ads
sign_up_verifiedevent fires only on the dashboard's code step. A verification on the consent page is server-rendered and does not push it (the server-side ChatGPT Ads report still runs).Cloud and self-hosted
POST /auth/verify-emailanswers 404.Type
Testing
New specs:
login.controller.verify-email.spec.ts(code step shown with nologin_user/amcp_signincookie, masked address, signed cookies; no second code within a minute; hourly cap; verified account goes to/callback; correct code completes; wrong, malformed code refused; CSRF and missing pending cookie refused; SSO-only meanwhile restarts; resend at most once a minute and within the hourly cap; continue after the link; social hand-over; every route refused without CSRF or the pending sign-in; self-hosted unchanged and 404 on every route) andemail-verification.service.spec.ts. The existing cloud fixtures inlogin.controller.spec.tsandlogin.controller.file-search.spec.tsnow sayemailVerified: true, since they model verified accounts.Commands, all in
packages/backend, afternpm ciat the root andnpx prisma generate:npm run lint: 0 errors, 11 warnings, all in files this PR does not touch. Theeslint --fixrun changed no files.npx tsc --noEmit -p tsconfig.json: exit 0.npx jest --maxWorkers=4 src/auth: 35 of 36 suites pass, 518 of 531 tests. The 13 failures are all injwt-or-management-key.guard.spec.ts, which needs a live PostgreSQL (prisma.organization.create()inbeforeAll). No database was running here; this is a baseline environment failure, not a regression.Checklist
npm test) — onlysrc/authwas run; see above for the DB-backed suiteRelated Issues
Fixes #1565