f75ffeed34d2a51f88fb338562bb7e1187ead08d max Tue Aug 18 06:30:37 2026 -0700 hgLogin: match OAuth/OpenID login on recovery email too, not just primary, refs #37984 When a provider returns a verified email, resolveIdentity() and the account chooser only matched it against gbMembers.email, so a user whose provider email is stored as their recovery address (recovEmail) could not auto-link or sign into that account. Password login and the passwordless email link already match email OR recovEmail; this brings social login in line. The provider email is guaranteed non-empty before the query runs (the existing isNotEmpty/isEmpty guards), so a blank recovEmail='' row can never match the empty string. Updated all three queries: the resolveIdentity auto-link, the OAuth branch of chooseAccountPage, and the chooseAccount confirmation, so the chooser cannot offer a row the finalize step rejects. diff --git src/hg/hgLogin/hgLogin.c src/hg/hgLogin/hgLogin.c index 7009e166211..17e30c1f304 100644 --- src/hg/hgLogin/hgLogin.c +++ src/hg/hgLogin/hgLogin.c @@ -2078,33 +2078,35 @@ clearPendingIdentity(); displayLoginPage(conn); return; } char *encEmail = htmlEncode(email); // the address is displayed; never trust it raw (XSS) char query[512]; if (emailMode) // Only the accounts that hold the just-validated login token, matching what emailLogin saw. sqlSafef(query, sizeof(query), "SELECT * FROM gbMembers WHERE (email='%s' OR recovEmail='%s') AND loginToken='%s' " "AND loginToken<>'' AND loginTokenExpires > NOW() AND accountActivated='Y' ORDER BY idx", email, email, cartUsualString(cart, "emailLogin_tokenMd5", "")); else // Only activated accounts, matching what chooseAccount() and resolveIdentity() accept; // otherwise the page offers a row the action refuses, and shows the username of an - // unactivated row anyone could have created with this address. + // unactivated row anyone could have created with this address. Match both primary and + // recovery address, as resolveIdentity() does; email is non-empty here (checked above). sqlSafef(query, sizeof(query), - "SELECT * FROM gbMembers WHERE email='%s' AND accountActivated='Y' ORDER BY idx", email); + "SELECT * FROM gbMembers WHERE (email='%s' OR recovEmail='%s') AND accountActivated='Y' " + "ORDER BY idx", email, email); struct gbMembers *list = gbMembersLoadByQuery(conn, query), *m; hPrintf("
The email address %s is associated with more than one %s account. " "Select the account you would like to sign in to.
", encEmail, brwName); else hPrintf("The email address %s is associated with more than one %s account. " "Select the account you would like to sign in to; your %s login will be linked to it.
", encEmail, brwName, oauthProviderLabel(provider)); hPrintf("%s", errMsg ? errMsg : ""); hPrintf("