c5fa25640623eef3aa9c8f1caaf4be18bab98a77
max
Tue Sep 15 05:45:03 2026 -0700
hgLogin: fixes from the code review of the social sign-in work
Read the provider label before clearPendingIdentity, not after. cartUsualString hands
back the cart's own string and cartRemove frees it, so the confirmation page was
naming the provider from memory that had just been released. hgLogin installs
pushCarefulMemHandler, which keeps the bytes readable, which is why it looked fine.
Let someone who mistyped their address at the choose-a-username page correct it. The
confirmation never arrives, and until now there was no way out at all: no password to
sign in with, no activated account for the email link, no login cookie for the
change-email page, and the user name and provider identity both already taken. The
confirmation page now offers a box to replace the address, authorized by the same
signed pending identity the account chooser uses, so only a real provider round trip
in this browser can get to it.
Signing in again while an account waits to be confirmed reuses the token that is still
outstanding instead of minting a new one. A new token silently voids the link in the
message before it, so a user who clicked the button twice and opened the first mail was
told the link was invalid.
Promise the account page only where it exists: the change-email flow is behind
login.emailLink, which is off by default, and since the address box went away a user
there would have had no way to change an address we told them they could change.
Clear the confirmation page's explanation variables in the plain signup path too. The
hand-off to that page is a JavaScript redirect, so a user who never lands on it leaves
them in the cart, and the next plain signup rendered somebody else's explanation over
an unrelated address.
diff --git src/hg/hgLogin/hgLogin.c src/hg/hgLogin/hgLogin.c
index d33f49ce175..a51106c0598 100644
--- src/hg/hgLogin/hgLogin.c
+++ src/hg/hgLogin/hgLogin.c
@@ -71,30 +71,31 @@
#define delayFraction 1.0 /* standard penalty is 1.0 for most CGIs */
/* Forward declarations for functions used before their definitions. */
static void printSocialButtons(boolean dividerAbove, boolean dividerBelow, char *action);
static void printEmailLinkButton();
static boolean emailLinkEnabled();
static boolean recovEmailChangeEnabled();
void changeRecovEmailPage(struct sqlConnection *conn);
static void printUsernameNote();
void emailLinkPage(struct sqlConnection *conn);
void displayLoginPage(struct sqlConnection *conn);
void displayAccHelpPage(struct sqlConnection *conn);
void completeAccountPage(struct sqlConnection *conn);
void sendEmailLink(struct sqlConnection *conn);
static void loginAndReturn(struct sqlConnection *conn, char *userName, uint idx);
+static boolean pendingIdentityValid();
/* ---- Global helper functions ---- */
char *browserName()
/* Return the browser name like 'UCSC Genome Browser' */
{
if isEmpty(cfgOption(CFG_LOGIN_BROWSER_NAME))
return cloneString("NULL_browserName");
else
return cloneString(cfgOption(CFG_LOGIN_BROWSER_NAME));
}
char *browserAddr()
/* Return the browser address like 'http://genome.ucsc.edu' */
{
if isEmpty(cfgOption(CFG_LOGIN_BROWSER_ADDR))
@@ -417,30 +418,46 @@
{
char *encProvider = htmlEncode(provider);
char *encAddress = htmlEncode(address);
if (isEmpty(existingUser))
hPrintf("
You signed in with %s, and %s did not tell us an email address, so we "
"asked you for one. No %s account uses %s yet, so we are making a new "
"account for it. Confirming the address is the last step.
Your %s sign-in belongs to the %s account %s, but the address on "
"that account, %s, has never been confirmed. Confirm it once and %s will "
"sign you straight in from then on.
",
encProvider, brwName, encUser, encAddress, encProvider);
+ /* If that address is wrong the confirmation can never arrive, and this account has no
+ * other way in, so offer to replace it here. pendingIdentityValid() is the check that
+ * this really is the person who just came back from the provider. */
+ if (pendingIdentityValid())
+ {
+ hPrintf("
If %s is not an address you can read, enter the right one and we "
+ "will send the confirmation there instead.
", returnURL);
cartRemove(cart, "hgLogin_email");
cartRemove(cart, "hgLogin_userName");
cartRemove(cart, "hgLogin_actMailProvider");
@@ -805,30 +822,50 @@
* (the email-link login token and the OAuth state nonce, both makeRandomKey) rather than from
* generateRandomPassword, whose output is far shorter and far less varied. The token is opaque
* -- it is hashed on the next line and only the hash is ever stored or mailed -- so nothing
* downstream depends on its shape. */
char *token = makeRandomKey(128+33);
char *tokenMD5 = generateTokenMD5(token);
sqlSafef(query,sizeof(query), "UPDATE gbMembers SET lastUse=NOW(),emailToken='%s', emailTokenExpires=DATE_ADD(NOW(), INTERVAL 7 DAY), accountActivated='N' WHERE userName='%s'",
tokenMD5,
username
);
sqlUpdate(conn, query);
sendActivateMail(email, username, tokenMD5);
return;
}
+void resendActivateMail(struct sqlConnection *conn, char *email, char *username)
+/* Mail the activation link for an account that already has one outstanding, reusing the token
+ * rather than minting a new one. Every fresh token silently kills the link in the mail before
+ * it, so a user who clicks the provider button twice and then opens the first message is told
+ * their link is invalid. Falls back to a new token once the old one has expired. */
+{
+char query[256];
+sqlSafef(query, sizeof(query),
+ "SELECT emailToken FROM gbMembers WHERE userName='%s' AND emailToken<>'' "
+ "AND emailTokenExpires > NOW()", username);
+char *token = sqlQuickString(conn, query);
+if (isEmpty(token))
+ {
+ setupNewAccount(conn, email, username);
+ return;
+ }
+sendActivateMail(email, username, token);
+freeMem(token);
+}
+
void printPwdEyeIcon(char *iconId, char *slashId)
/* print a clickable eye icon as a normal sibling right after a password
* input (not overlapping it); slashId is the toggled to show
* "hidden". No-op if disabled via hg.conf login.pwdEyeIcon. */
{
if (!pwdEyeIconEnabled)
return;
hPrintf(
""
"