fix(oauth): honor connectionId on token refresh so email-less providers don't duplicate (#8062)

persistOAuthConnection gated its whole dedup step behind if(tokenData.email).
The matcher (findExistingOAuthConnectionMatch) already matches by explicit
connectionId first, but it was never reached when the payload had no top-level
email. GitHub Copilot's device-code flow keeps identity under
providerSpecificData.githubEmail, so tokenData.email is undefined — a refresh
(which passes the existing connectionId) skipped the match and fell through to
createProviderConnection, producing a duplicate connection.

- Widen the gate to if(connectionId || tokenData.email) so an explicit
  connectionId is honored regardless of email.
- Guard the matcher's email branch with if(!tokenData.email) return false, so a
  widened gate can't false-match an email-less connection via
  safeEqual(undefined, undefined).

Fixes #8059.
This commit is contained in:
Innokentiy Solntsev
2026-07-22 22:37:54 +02:00
committed by GitHub
parent 1a076464f0
commit f17d23bf0b
3 changed files with 101 additions and 1 deletions

View File

@@ -68,6 +68,10 @@ export function findExistingOAuthConnectionMatch(
): Record<string, any> | undefined {
return existing.find((c) => {
if (c.id && safeEqual(connectionId, c.id)) return true;
// Email dedup only when the payload actually carries an email. Without this
// guard `safeEqual(undefined, undefined)` is true, so an email-less payload
// would false-match the first email-less connection of the provider.
if (!tokenData.email) return false;
if (!safeEqual(c.email, tokenData.email) || c.authType !== "oauth") return false;
if (provider === "codex") {
return isSameCodexAccount(c.providerSpecificData, tokenData.providerSpecificData);
@@ -130,7 +134,12 @@ export async function persistOAuthConnection(
: null;
let connection: any;
if (tokenData.email) {
// A connectionId is an explicit "update THIS connection" signal (token refresh
// / re-auth of a known connection); honor it even when the payload has no
// top-level email. Some providers (e.g. GitHub Copilot) keep identity under
// providerSpecificData, so gating dedup on tokenData.email alone created a
// duplicate connection on every refresh (#8059).
if (connectionId || tokenData.email) {
const existing = await getProviderConnections({ provider });
const match = findExistingOAuthConnectionMatch(existing, provider, tokenData, connectionId);
const matchId = typeof match?.id === "string" ? match.id : null;