Skip to content

Connect logins that share a verified email, after the owner confirms - #3

Merged
guohai merged 3 commits into
mainfrom
feat/email-account-link
Oct 11, 2026
Merged

guohai merged 3 commits into
mainfrom
feat/email-account-link

Conversation

@guohai

@guohai guohai commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Why

A person who signs in with Google one day and GitHub the next, using the same email, should end up as one Agora.Build user. Until now, signing in with a second provider created a second account unless the person had first connected it from the Account page.

What changes

  • Verified emails per login:
    • GitHub: the primary email, and only when /user/emails marks it verified. This adds the user:email scope.
    • Google, Apple and Agora OIDC: email_verified.
    • Agora OAuth: none, until Agora SSO returns a stable user ID.
    • Only plain ASCII addresses match.
    • The email is refreshed at each sign-in; if GitHub's lookup fails, the stored email is kept.
  • Confirm, then link. A new login whose verified email already belongs to an account gets no session and creates no account. It is held for 10 minutes, and the browser can:
    1. sign in with one of the existing account's logins, then confirm "Connect Google (a***@example.com) to this account?", which is a same-origin POST;
    2. create a separate account instead; or
    3. discard it.
  • Hardening:
    • The hold cookie is __Host-house_link over HTTPS, so a sibling subdomain can't plant one.
    • A confirming sign-in started from another site is refused.
    • Only the very session that confirmed can connect.
    • Signing in to a different account discards the hold.
    • An account can't take two logins from the same provider.
  • Privacy: emails are visible only to their owner in a browser session (/api/me, Account page). Apps never see them: not through /oauth/userinfo, not through an app token reading /api/me, and not through identity lookup, which now returns only provider, issuer and subject.
  • Docs: README and docs/platform.md describe the flow.

Accounts that already share an email are not merged. This only prevents new duplicates.

Verification

  • npm test: 66 pass, 0 fail, 1 skipped (PostgreSQL, which needs a database). The new tests cover:
    • the hold, ready, connect, separate, discard, lapse and mismatch paths
    • a provider conflict, and another session or account trying to connect
    • a cross-site confirm and a forged-origin POST
    • the __Host- cookie, and no emails in identity lookup or app reads
    • GitHub email edge cases
  • Merged with feat/stoa-world (PR Add the Stoa: walkable spaces with live presence, rooms, and calls #2): 294 pass. Deployed locally with PostgreSQL and checked: pages load, and the pending-link endpoint answers.
  • Both dialogs checked in headless Chromium at 1280 and 390 px with no page errors.
  • Two security reviews:
    • The first found an email leak through identity lookup, plus a confirm step that wasn't enforced server-side. Both are fixed.
    • The second rated it safe to ship, and its minor hardening points are included.

Known limits

  • Someone who controls a person's email at another provider can, on a shared computer, leave the confirm dialog open for them. The dialog shows the held login's name and masked email to make that visible.
  • If GitHub's email lookup fails during a brand-new sign-in, that login gets its own account. This fails safe.

Generated with SMT smt@agora.build

A new login whose provider-verified email already belongs to an account no
longer creates a second account. It is held for ten minutes in this browser
and connected only when the browser signs in with one of the account's
existing logins. Unverified emails never match; emails stay private to the
owner and are never shown to applications.

🤖 Built with SMT <smt@agora.build>
Signing in to the matching account now only readies the held login; the
portal then asks to connect it and does so on a same-origin POST. The hold
cookie is __Host- prefixed over HTTPS, confirming sign-ins are refused from
other sites, and a held login can become a separate account instead.
Identity lookup no longer returns emails, a failed GitHub email lookup keeps
the stored email, and only ASCII addresses match.

🤖 Built with SMT <smt@agora.build>
@guohai
guohai merged commit 1a59c26 into main Oct 11, 2026
4 checks passed
@guohai
guohai deleted the feat/email-account-link branch October 11, 2026 05:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant