4b04bd017d7a36d460447d8552ecaf9c8a33db4a
max
  Tue Aug 4 02:51:56 2026 -0700
hgLogin: gate email-link sign-in and change-email behind login.emailLink (default off), refs #37929

Also: GitHub/OIDC token-format and robustness fixes, signed pending-identity to close
an account-takeover hole in the OAuth account chooser, account chooser for the email-link
flow, idx-based chooser to avoid a utf8/latin1 collation error on non-ASCII usernames, and
login/signup page UI polish (consistent buttons, fonts, cache-busted stylesheet, forgot
links, wording). refs #37984

diff --git src/hg/hgLogin/hgLogin.c src/hg/hgLogin/hgLogin.c
index f7cc0fdc79f..4ff08174b69 100644
--- src/hg/hgLogin/hgLogin.c
+++ src/hg/hgLogin/hgLogin.c
@@ -52,32 +52,33 @@
 char *errMsg = NULL;    /* Error message to show user when form data rejected */
 char brwName[64];
 char brwAddr[256];
 char signature[256];
 char returnAddr[256];
 char *hgLoginUrl = NULL; /* full absolute URL to hgLogin as seen from browser,
     e.g. http://genome.ucsc.edu/cgi-bin/hgLogin. Can be a relative URL /cgi-bin/hgLogin if
     hg.conf login.relativeLink is on. */
 boolean pwdEyeIconEnabled = TRUE; /* show/hide eye icon on password fields;
     set from hg.conf login.pwdEyeIcon in doMiddle() */
 
 /* for earlyBotCheck() function at the beginning of main() */
 #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);
+static void printSocialButtons(boolean dividerAbove, boolean dividerBelow, char *action);
 static void printEmailLinkButton();
+static boolean emailLinkEnabled();
 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);
 
 /* ---- 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));
@@ -603,59 +604,68 @@
     " document.getElementById('usernameBox').style.display='inline';\n"
     " document.getElementById('emailAddrBox').style.display='none';\n"
     " }\n"
     "}\n"
     );
 hPrintf("<div id=\"accountHelpBox\" class=\"centeredContainer formBox\">"
     "\n"
     "<h2>%s</h2>"
     "\n", brwName);
 hPrintf("<h3>Having trouble signing in?</h3>"
     "\n"
     "<form method=post action=\"%s\" name=\"accountLoginForm\" id=\"acctHelpForm\">"
     "\n"
     "<p><span style='color:red;'>%s</span><p>"
     "\n", hgLoginUrl, errMsg ? errMsg : "");
-hPrintf("<div class=\"inputGroup\">"
-    "<div class=\"acctHelpSection\"><input name=\"hgLogin_helpWith\" type=\"radio\" value=\"password\" id=\"password\">"
-    "<label for=\"password\" class=\"radioLabel\">I forgot my <b>password</b>. Send me a new one.</label></div>"
-    "<div class=\"acctHelpSection\"><input name=\"hgLogin_helpWith\" type=\"radio\" value=\"username\" id=\"username\">"
-    "<label for=\"username\" class=\"radioLabel\">I forgot my <b>username</b>. Please email it to me.</label></div>"
-    "<div class=\"acctHelpSection\"><input name=\"hgLogin_helpWith\" type=\"radio\" value=\"loginLink\" id=\"loginLink\">"
-    "<label for=\"loginLink\" class=\"radioLabel\">Email me a <b>login link</b> so I can sign in without a password.</label></div>"
-    "\n"
-    "</div>"
-    "\n");
+// A "Forgot username/password" link may preselect a radio via hgLogin_helpWith in the URL.
+char *pre = cartUsualString(cart, "hgLogin_helpWith", "");
+hPrintf("<div class=\"inputGroup\">");
+hPrintf("<div class=\"acctHelpSection\"><input name=\"hgLogin_helpWith\" type=\"radio\" value=\"password\" id=\"password\"%s>"
+    "<label for=\"password\" class=\"radioLabel\">I forgot my <b>password</b>. Send me a new one.</label></div>",
+    sameString(pre, "password") ? " checked" : "");
+hPrintf("<div class=\"acctHelpSection\"><input name=\"hgLogin_helpWith\" type=\"radio\" value=\"username\" id=\"username\"%s>"
+    "<label for=\"username\" class=\"radioLabel\">I forgot my <b>username</b>. Please email it to me.</label></div>",
+    sameString(pre, "username") ? " checked" : "");
+if (emailLinkEnabled())
+    hPrintf("<div class=\"acctHelpSection\"><input name=\"hgLogin_helpWith\" type=\"radio\" value=\"loginLink\" id=\"loginLink\">"
+        "<label for=\"loginLink\" class=\"radioLabel\">Email me a <b>login link</b> so I can sign in without a password.</label></div>");
+hPrintf("</div>\n");
 hPrintf("<div class=\"inputGroup\" id=\"usernameBox\" style=\"display: none;\">"
     "<label for=\"emailUsername\">Username</label>"
     "<input type=\"text\" name=\"hgLogin_userName\" value=\"%s\" size=\"30\" id=\"emailUsername\">"
     "</div>"
     "\n"
     "<div class=\"inputGroup\" id=\"emailAddrBox\" style=\"display: none;\">"
     "<label for=\"emailPassword\">Email address</label>"
     "<input type=\"text\" name=\"hgLogin_email\" value=\"%s\" size=\"30\" id=\"emailPassword\">"
     "</div>"
     "\n"
     "<div class=\"formControls\">"
     "    <input type=\"submit\" name=\"hgLogin.do.accountHelp\" value=\"Continue\" class=\"largeButton\">"
-    "     &nbsp;<a href=\"%s\">Cancel</a>"
+    "     &nbsp;<a href=\"%s\" class=\"cancelButton\">Cancel</a>"
     "</div>"
     "</form>"
     "</div><!-- END - accountHelpBox -->", username, email, getReturnToURL());
 jsOnEventById("click", "password", "toggle('showU');");
 jsOnEventById("click", "username", "toggle('showE');");
+if (emailLinkEnabled())
     jsOnEventById("click", "loginLink", "toggle('showE');");
+// If a radio was preselected via the URL, open its matching input box on load.
+if (sameString(pre, "password"))
+    jsInline("toggle('showU');");
+else if (sameString(pre, "username") || sameString(pre, "loginLink"))
+    jsInline("toggle('showE');");
 cartSaveSession(cart);
 }
 
 void sendNewPassword(struct sqlConnection *conn, char *username, char *password)
 /* email user new password  */
 {
 char query[256];
 /* find email address associated with this username */
 sqlSafef(query,sizeof(query),"SELECT email FROM gbMembers WHERE userName='%s'", username);
 char *email = sqlQuickString(conn, query);
 
 if (!email || sameString(email,""))
     {
     freez(&errMsg);
     errMsg = cloneString("Email address not found.");
@@ -765,76 +775,75 @@
     "  }\n"
     "}\n"
     );
 }
 
 void displayLoginPage(struct sqlConnection *conn)
 /* draw the account login page */
 {
 char *username = cartUsualString(cart, "hgLogin_userName", "");
 hPrintf("<div id=\"loginBox\" class=\"centeredContainer formBox\">"
     "\n"
     "<h2>%s</h2>"
     "\n", brwName);
 hPrintf(
     "<h3>Login</h3>"
-    "\n");
+    "<p>Do not have an account? <a href=\"%s?hgLogin.do.signupPage=1\">Go to the sign up page</a>.</p>"
+    "\n", hgLoginUrl);
 if (errMsg && sameString(errMsg, "Your account has been activated."))
     hPrintf("<span style='color:green;'>%s</span>\n", errMsg ? errMsg : "");
 else
     hPrintf("<span style='color:red;'>%s</span>\n", errMsg ? errMsg : "");
 hPrintf("<form method=post action=\"%s\" name=\"accountLoginForm\" id=\"accountLoginForm\">"
     "\n"
     "<div class=\"inputGroup\">"
     "<label for=\"userName\">Username</label>"
     "<input type=text name=\"hgLogin_userName\" value=\"%s\" size=\"30\" id=\"userName\">"
+    "<a class=\"forgotLink\" href=\"%s?hgLogin.do.displayAccHelpPage=1&hgLogin_helpWith=username\">Forgot username</a>"
     "</div>"
     "\n"
     "<div class=\"inputGroup\">"
     "<label for=\"password\">Password</label>"
     "<span style=\"position:relative; display:inline-block;\">"
     "<input type=password name=\"hgLogin_password\" value=\"\" size=\"30\" id=\"password\">"
-    , hgLoginUrl, username);
+    , hgLoginUrl, username, hgLoginUrl);
 printPwdEyeIcon("pwdEyeIcon", "pwdEyeSlash");
 hPrintf(
     "</span>"
+    "<a class=\"forgotLink\" href=\"%s?hgLogin.do.displayAccHelpPage=1&hgLogin_helpWith=password\">Forgot password</a>"
     "</div>"
     "\n"
     "<div class=\"formControls\">"
     "   <input type=\"submit\" name=\"hgLogin.do.displayLogin\" value=\"Login\" class=\"largeButton\">"
-    "    &nbsp;<a href=\"%s\">Cancel</a>"
+    "    &nbsp;<a href=\"%s\" class=\"cancelButton\">Cancel</a>"
     "</div>"
-    , getReturnToURL());
+    , hgLoginUrl, getReturnToURL());
 if (pwdEyeIconEnabled)
     {
     printPwdToggleJS();
     jsOnEventById("click", "pwdEyeIcon", "togglePwdVisibility('password','pwdEyeSlash');");
     }
 cartSaveSession(cart);
 hPrintf("</form>\n");
 printEmailLinkButton();
-printSocialButtons(TRUE, FALSE);
+printSocialButtons(TRUE, FALSE, "Sign in");
 hPrintf(
-    "<div id=\"helpBox\">"
-    "<a href=\"%s?hgLogin.do.displayAccHelpPage=1\">Forgot username, password or sign in with an email link</a><br>"
-    "Need an account? <a href=\"%s?hgLogin.do.signupPage=1\">Sign up</a>.<br>"
-    "</div><!-- END - helpBox -->"
     "</div><!-- END - loginBox -->"
     "\n"
     "\n"
     "</body>"
-    "</html>", hgLoginUrl, hgLoginUrl);
+    "</html>");
 }
 
 void activateAccount(struct sqlConnection *conn)
 /* activate account */
 {
 char query[256];
 char *token = cgiUsualString("token", "");
 char *username = cgiUsualString("user","");
 sqlSafef(query,sizeof(query),
     "SELECT emailToken FROM gbMembers WHERE userName='%s'", username);
 char *emailToken = sqlQuickString(conn, query);
 if (sameString(emailToken, token))
     {
     sqlSafef(query,sizeof(query), "UPDATE gbMembers SET lastUse=NOW(), dateActivated=NOW(), emailToken='', emailTokenExpires='', accountActivated='Y' WHERE userName='%s'",
     username);
@@ -892,31 +901,31 @@
 hPrintf(
     "</span>"
     "</div>"
     "\n"
     "<div class=\"inputGroup\">"
     "<label for=\"newPw2\">Re-enter New Password</label>"
     "<span style=\"position:relative; display:inline-block;\">"
     "<input type=\"password\" name=\"hgLogin_newPassword2\" value=\"\" size=\"30\" id=\"newPw2\">");
 printPwdEyeIcon("newPw2EyeIcon", "newPw2EyeSlash");
 hPrintf(
     "</span>"
     "</div>"
     "\n"
     "<div class=\"formControls\">"
     "    <input type=\"submit\" name=\"hgLogin.do.changePassword\" value=\"Change Password\" class=\"largeButton\"> &nbsp; "
-    "    <a href=\"%s\">Cancel</a>"
+    "    <a href=\"%s\" class=\"cancelButton\">Cancel</a>"
     "\n"
     "</div>"
     "</form>"
     "\n"
     "</div><!-- END - changePwBox -->"
     "\n", getReturnToURL());
 if (pwdEyeIconEnabled)
     {
     printPwdToggleJS();
     jsOnEventById("click", "curPwEyeIcon", "togglePwdVisibility('currentPw','curPwEyeSlash');");
     jsOnEventById("click", "newPw1EyeIcon", "togglePwdVisibility('newPw1','newPw1EyeSlash');");
     jsOnEventById("click", "newPw2EyeIcon", "togglePwdVisibility('newPw2','newPw2EyeSlash');");
     }
 cartSaveSession(cart);
 }
@@ -1007,67 +1016,77 @@
 cartRemove(cart, "hgLogin_newPassword1");
 cartRemove(cart, "hgLogin_newPassword2");
 sqlSafef(query,sizeof(query),"SELECT * FROM gbMembers WHERE userName='%s'", user);
 struct gbMembers *m = gbMembersLoadByQuery(conn, query);
 struct dyString *cookieJS = getLoginCookieJS(user, m->idx);
 jsInline(cookieJS->string);
 returnToURL(150);
 }
 
 void changeEmailPage(struct sqlConnection *conn)
 /* Draw the change-email page for the currently logged-in user.  The account is taken from
  * the validated login cookie (wikiLinkUserName), never from a form field, so a user can only
  * change their own email.  Being logged in is the authorization; no password is required,
  * which also lets social-login accounts (which have no password) change their email. */
 {
+if (!emailLinkEnabled())
+    {
+    displayLoginPage(conn);
+    return;
+    }
 char *user = wikiLinkUserName();
 if (isEmpty(user))
     {
     freez(&errMsg);
     errMsg = cloneString("Please log in first to change your email address.");
     displayLoginPage(conn);
     return;
     }
 char query[256];
 sqlSafef(query, sizeof(query), "SELECT email FROM gbMembers WHERE userName='%s'", user);
 char *curEmail = sqlQuickString(conn, query);
 
 hPrintf("<div id=\"changeEmailBox\" class=\"centeredContainer formBox\">"
     "<h2>%s</h2>", brwName);
 hPrintf("<h3>Change Email</h3>");
 hPrintf("<p><span style='color:red;'>%s</span></p>", errMsg ? errMsg : "");
 hPrintf("<form method=\"post\" action=\"%s\" name=\"changeEmailForm\">", hgLoginUrl);
 hPrintf("<p>Signed in as <b>%s</b>.<br>Current email address: <b>%s</b></p>",
     user, isNotEmpty(curEmail) ? curEmail : "(none)");
 hPrintf("<div class=\"inputGroup\">"
     "<label for=\"newEmail1\">New email address</label>"
     "<input type=\"text\" name=\"hgLogin_newEmail1\" value=\"\" size=\"30\" id=\"newEmail1\">"
     "</div>");
 hPrintf("<div class=\"inputGroup\">"
     "<label for=\"newEmail2\">Re-enter new email address</label>"
     "<input type=\"text\" name=\"hgLogin_newEmail2\" value=\"\" size=\"30\" id=\"newEmail2\">"
     "</div>");
 hPrintf("<div class=\"formControls\">"
     "<input type=\"submit\" name=\"hgLogin.do.changeEmail\" value=\"Change Email\" class=\"largeButton\">"
-    " &nbsp;<a href=\"%s\">Cancel</a>"
+    " &nbsp;<a href=\"%s\" class=\"cancelButton\">Cancel</a>"
     "</div></form></div><!-- END - changeEmailBox -->", getReturnToURL());
 cartSaveSession(cart);
 }
 
 void changeEmail(struct sqlConnection *conn)
 /* Process the change-email form for the currently logged-in user. */
 {
+if (!emailLinkEnabled())
+    {
+    displayLoginPage(conn);
+    return;
+    }
 char *user = wikiLinkUserName();
 if (isEmpty(user))
     {
     freez(&errMsg);
     errMsg = cloneString("Please log in first to change your email address.");
     displayLoginPage(conn);
     return;
     }
 char *email1 = cartUsualString(cart, "hgLogin_newEmail1", "");
 char *email2 = cartUsualString(cart, "hgLogin_newEmail2", "");
 if (isEmpty(email1) || spc_email_isvalid(email1) == 0)
     {
     freez(&errMsg);
     errMsg = cloneString("Please enter a valid email address.");
     changeEmailPage(conn);
@@ -1086,36 +1105,35 @@
 sqlUpdate(conn, query);
 cartRemove(cart, "hgLogin_newEmail1");
 cartRemove(cart, "hgLogin_newEmail2");
 hPrintf("<div class=\"centeredContainer formBox\"><h2>%s</h2>", brwName);
 hPrintf("<h3>Your email address has been changed.</h3>");
 hPrintf("<p>Your email address is now <b>%s</b>.</p></div>", email1);
 returnToURL(1500);
 }
 
 void signupPage(struct sqlConnection *conn)
 /* draw the signup page */
 {
 hPrintf("<div id=\"signUpBox\" class=\"centeredContainer formBox\">"
     "<h2>%s</h2>", brwName);
 hPrintf(
-    "<p>Signing up enables you to save multiple sessions and to share your sessions with others.</p>"
+    "<p>Signing up enables you to save multiple sessions, share your sessions with others via short and stable session links and manage previously uploaded custom tracks and track hubs.</p>"
     "\n");
-hPrintf("<a class=\"socialButton\" href=\"%s?hgLogin.do.displayLoginPage=1\">"
-    "Sign in with username and password</a>", hgLoginUrl);
-printEmailLinkButton();
-printSocialButtons(TRUE, TRUE);
+hPrintf("<p>Already have an account? "
+    "<a href=\"%s?hgLogin.do.displayLoginPage=1\">Go to the login page</a>.</p>", hgLoginUrl);
+printSocialButtons(FALSE, TRUE, "Sign up");
 hPrintf("<h3>Sign Up Using Email</h3>"
     "<form method=\"post\" action=\"%s\" name=\"mainForm\">"
     "<span style='color:red;'>%s</span>"
     "\n", hgLoginUrl, errMsg ? errMsg : "");
 printUsernameNote();
 hPrintf("<div class=\"inputGroup\">"
     "<label for=\"userName\">Username</label>"
     "<input type=text name=\"hgLogin_userName\" value=\"%s\" size=\"30\" id=\"userName\">"
     "</div>"
     "\n"
     "<div class=\"inputGroup\">"
     "<label for=\"emailAddr\">Email address</label>"
     "<input type=text name=\"hgLogin_email\" value=\"%s\" size=\"30\" id=\"emailAddr\">"
     "</div>"
     "\n"
@@ -1144,31 +1162,31 @@
     "</div>"
     "\n"
     "<div class=\"inputGroup\">"
     "<label for=\"passwordCheck\">Re-enter Password</label>"
     "<span style=\"position:relative; display:inline-block;\">"
     "<input type=password name=\"hgLogin_password2\" value=\"%s\" size=\"30\" id=\"passwordCheck\">",
     cartUsualString(cart, "hgLogin_password2", ""));
 printPwdEyeIcon("signupPwCheckEyeIcon", "signupPwCheckEyeSlash");
 hPrintf(
     "</span>"
     "\n"
     "</div>"
     "\n"
     "<div class=\"formControls\">"
     "    <input type=\"submit\" name=\"hgLogin.do.signup\" value=\"Sign Up using Email\" class=\"largeButton\"> &nbsp; "
-    "    <a href=\"%s\">Cancel</a>"
+    "    <a href=\"%s\" class=\"cancelButton\">Cancel</a>"
     "</div>"
     "</form>"
     "</div><!-- END - signUpBox -->",
     getReturnToURL());
 if (pwdEyeIconEnabled)
     {
     printPwdToggleJS();
     jsOnEventById("click", "signupPwEyeIcon", "togglePwdVisibility('password','signupPwEyeSlash');");
     jsOnEventById("click", "signupPwCheckEyeIcon", "togglePwdVisibility('passwordCheck','signupPwCheckEyeSlash');");
     }
 cartSaveSession(cart);
 }
 
 void signup(struct sqlConnection *conn)
 /* process the signup form */
@@ -1505,58 +1523,65 @@
     "<p align=\"left\">"
     "</p>"
     "<span style='color:red;'></span>"
     "\n");
 struct dyString *javascript = dyStringNew(1024);
 struct slName *newCookies = loginLogoutUser(), *sl;
 for (sl = newCookies;  sl != NULL;  sl = sl->next)
     dyStringPrintf(javascript, " document.cookie = '%s';", sl->name);
 jsInline(javascript->string);
 /* return to "returnto" URL */
 returnToURL(150);
 }
 
 /* ---- Social login (OAuth) and passwordless email-link login ---- */
 
-static void printSocialButtons(boolean dividerAbove, boolean dividerBelow)
-/* Print sign-in buttons for any enabled social login providers, optionally bracketed by "or"
- * dividers.  The login page uses only the top divider (separating the buttons from the
- * password form above); the signup page uses both, so the buttons sit in their own section
- * between the "already have an account" link and the email signup form.  Prints nothing if no
- * provider is configured, so mirrors without OAuth credentials are unaffected. */
+static void printSocialButtons(boolean dividerAbove, boolean dividerBelow, char *action)
+/* Print social login buttons for any enabled providers, optionally bracketed by "or"
+ * dividers.  action is the button verb ("Sign in" on the login page, "Sign up" on the signup
+ * page).  Prints nothing if no provider is configured, so mirrors without OAuth credentials
+ * are unaffected. */
 {
 if (!oauthAnyProviderEnabled())
     return;
 hPrintf("<div class=\"socialLogin\">");
 if (dividerAbove)
     hPrintf("<div class=\"orDivider\"><span>or</span></div>");
 struct slName *prov, *providers = oauthProviderNames();
 for (prov = providers;  prov != NULL;  prov = prov->next)
     hPrintf("<a class=\"socialButton\" href=\"%s?hgLogin.do.oauthStart=1&provider=%s\">"
-            "Sign in with %s</a>",
-            hgLoginUrl, cgiEncode(prov->name), oauthProviderLabel(prov->name));
+            "%s with %s</a>",
+            hgLoginUrl, cgiEncode(prov->name), action, oauthProviderLabel(prov->name));
 if (dividerBelow)
     hPrintf("<div class=\"orDivider\"><span>or</span></div>");
 hPrintf("</div>");
 }
 
+static boolean emailLinkEnabled()
+/* Return TRUE if passwordless email-link login is turned on in hg.conf.  It needs working
+ * outbound email, so it is off unless the admin explicitly enables it with login.emailLink=on. */
+{
+return cfgOptionBooleanDefault(CFG_LOGIN_EMAIL_LINK, FALSE);
+}
+
 static void printEmailLinkButton()
-/* Print a grey button that opens the passwordless email-link login page.  Always available
- * (this is core hgLogin functionality, independent of any OAuth configuration). */
+/* Print a grey button that opens the passwordless email-link login page, if enabled. */
 {
+if (!emailLinkEnabled())
+    return;
 hPrintf("<a class=\"socialButton\" href=\"%s?hgLogin.do.emailLinkPage=1\">"
-    "Sign in with an email link</a>", hgLoginUrl);
+    "Email me a sign-in link</a>", hgLoginUrl);
 }
 
 static void printUsernameNote()
 /* Print a short hint, shown wherever a new username is chosen, explaining that the username
  * shows up in every short link the user later creates, so it should be short and easy to type. */
 {
 hPrintf("<p style=\"font-size:0.9em\">Note: your username becomes part of every short link "
     "you create later (for example <code>%s/s/<b>username</b>/MySession</code>), so choose "
     "something short and easy to type.</p>", brwAddr);
 }
 
 static void loginAndReturn(char *userName, uint idx)
 /* Set the permanent login cookies for userName and bounce back to the returnto URL.
  * Every login method (password, social, email link) funnels through here, so they all
  * produce the same long-lived login cookies. */
@@ -1637,107 +1662,140 @@
 
 static struct gbMembers *memberForIdentity(struct sqlConnection *conn, struct oauthIdentity *id)
 /* Return the gbMembers account already linked to this provider identity, or NULL. */
 {
 char query[512];
 sqlSafef(query, sizeof(query),
     "SELECT idx FROM gbMemberIdentity WHERE provider='%s' AND subject='%s'",
     id->provider, id->subject);
 uint idx = (uint)sqlQuickLongLong(conn, query);
 if (idx == 0)
     return NULL;
 sqlSafef(query, sizeof(query), "SELECT * FROM gbMembers WHERE idx=%u", idx);
 return gbMembersLoadByQuery(conn, query);
 }
 
+static char *oauthPendingSig(char *provider, char *subject, char *email)
+/* Signature over a pending social identity, keyed by the secret login.cookieSalt.  Only
+ * resolveIdentity (which runs after a genuine provider verification) can produce a valid one,
+ * so a pending identity injected through cart/CGI variables will not validate.  (Not bound to
+ * the session id: the hgsid is regenerated across the provider redirect, so a session-bound
+ * signature would never match on the way back.)  Result is allocd. */
+{
+char buf[1024];
+safef(buf, sizeof(buf), "%s|%s|%s|%s",
+    emptyForNull(cfgOption(CFG_LOGIN_COOKIE_SALT)),
+    emptyForNull(provider), emptyForNull(subject), emptyForNull(email));
+return generateTokenMD5(buf);
+}
+
+static boolean pendingIdentityValid()
+/* TRUE only if the pending-identity cart variables carry a signature we minted this session.
+ * Guards the OAuth chooser and completeAccount against forged/injected pending identities. */
+{
+char *sig = cartUsualString(cart, "oauth_pending_sig", "");
+if (isEmpty(sig))
+    return FALSE;
+char *expected = oauthPendingSig(cartUsualString(cart, "oauth_pending_provider", ""),
+                                 cartUsualString(cart, "oauth_pending_subject", ""),
+                                 cartUsualString(cart, "oauth_pending_email", ""));
+boolean ok = sameString(sig, expected);
+freeMem(expected);
+return ok;
+}
+
 static void setPendingIdentity(struct oauthIdentity *id)
 /* Stash an authenticated-but-not-yet-linked identity in the cart so it survives a form
- * round-trip (the "choose a username" or "choose an account" page). */
+ * round-trip (the "choose a username" or "choose an account" page).  The signature is what
+ * proves, on the way back, that we really verified this identity. */
 {
 cartSetString(cart, "oauth_pending_provider", id->provider);
 cartSetString(cart, "oauth_pending_subject", id->subject);
 cartSetString(cart, "oauth_pending_email", emptyForNull(id->email));
 cartSetString(cart, "oauth_pending_name", emptyForNull(id->displayName));
+cartSetString(cart, "oauth_pending_sig",
+    oauthPendingSig(id->provider, id->subject, emptyForNull(id->email)));
 }
 
 static void clearPendingIdentity()
 /* Remove the pending-identity cart variables once the account is linked. */
 {
 cartRemove(cart, "oauth_pending_provider");
 cartRemove(cart, "oauth_pending_subject");
 cartRemove(cart, "oauth_pending_email");
 cartRemove(cart, "oauth_pending_name");
+cartRemove(cart, "oauth_pending_sig");
 }
 
 static void linkIdentity(struct sqlConnection *conn, uint idx, struct oauthIdentity *id)
 /* Insert or refresh the gbMemberIdentity row linking idx to this provider identity. */
 {
 char query[1024];
 char *email = emptyForNull(id->email);
 sqlSafef(query, sizeof(query),
     "INSERT INTO gbMemberIdentity SET idx=%u, provider='%s', subject='%s', email='%s', "
     "created=NOW(), lastUse=NOW() "
     "ON DUPLICATE KEY UPDATE idx=%u, email='%s', lastUse=NOW()",
     idx, id->provider, id->subject, email, idx, email);
 sqlUpdate(conn, query);
 }
 
 void completeAccountPage(struct sqlConnection *conn)
 /* Ask a first-time social-login user to confirm a username (and email) for a new account. */
 {
 char *provider = cartUsualString(cart, "oauth_pending_provider", "");
 char *email = cartUsualString(cart, "oauth_pending_email", "");
 char *name = cartUsualString(cart, "oauth_pending_name", "");
-if (isEmpty(provider))
+if (isEmpty(provider) || !pendingIdentityValid())
     {
     displayLoginPage(conn);
     return;
     }
 char *suggested = cartUsualString(cart, "hgLogin_userName", "");
 if (isEmpty(suggested))
     suggested = suggestUsername(conn, email, name);
 
 hPrintf("<div id=\"completeAccountBox\" class=\"centeredContainer formBox\">"
     "<h2>%s</h2>", brwName);
 hPrintf("<h3>Choose a username</h3>");
 hPrintf("<p>You signed in with %s. Pick a username for your new %s account. "
     "You can change the suggested name below.</p>",
     oauthProviderLabel(provider), brwName);
 printUsernameNote();
 hPrintf("<span style='color:red;'>%s</span>", errMsg ? errMsg : "");
 hPrintf("<form method=\"post\" action=\"%s\" name=\"completeAccountForm\">", hgLoginUrl);
 hPrintf("<div class=\"inputGroup\">"
     "<label for=\"userName\">Username</label>"
     "<input type=\"text\" name=\"hgLogin_userName\" value=\"%s\" size=\"30\" id=\"userName\">"
     "</div>", suggested);
 hPrintf("<div class=\"inputGroup\">"
     "<label for=\"emailAddr\">Email address</label>"
     "<input type=\"text\" name=\"hgLogin_email\" value=\"%s\" size=\"30\" id=\"emailAddr\">"
     "</div>", email);
 hPrintf("<div class=\"formControls\">"
     "<input type=\"submit\" name=\"hgLogin.do.completeAccount\" value=\"Create Account\" class=\"largeButton\">"
-    " &nbsp;<a href=\"%s\">Cancel</a>"
+    " &nbsp;<a href=\"%s\" class=\"cancelButton\">Cancel</a>"
     "</div></form></div><!-- END - completeAccountBox -->", getReturnToURL());
 cartSaveSession(cart);
 }
 
 void completeAccount(struct sqlConnection *conn)
 /* Create the account for a first-time social-login user, link the identity, and log in. */
 {
 char *provider = cartUsualString(cart, "oauth_pending_provider", "");
 char *subject = cartUsualString(cart, "oauth_pending_subject", "");
-if (isEmpty(provider) || isEmpty(subject))
+if (isEmpty(provider) || isEmpty(subject) || !pendingIdentityValid())
     {
     freez(&errMsg);
     errMsg = cloneString("Your login session expired. Please sign in again.");
     displayLoginPage(conn);
     return;
     }
 char *user = cartUsualString(cart, "hgLogin_userName", "");
 char *encUserName = cgiEncodeFull(user);
 if (isEmpty(user))
     {
     freez(&errMsg);
     errMsg = cloneString("Please enter a username.");
     completeAccountPage(conn);
     return;
     }
@@ -1788,107 +1846,161 @@
 dyStringFree(&q);
 uint idx = sqlLastAutoId(conn);
 
 struct oauthIdentity pending;
 ZeroVar(&pending);
 pending.provider = provider;
 pending.subject = subject;
 pending.email = email;
 linkIdentity(conn, idx, &pending);
 
 clearPendingIdentity();
 loginAndReturn(user, idx);
 }
 
 void chooseAccountPage(struct sqlConnection *conn)
-/* When a verified OAuth email matches several accounts, ask the user which one to sign
- * in to (and link this social identity to). */
+/* Ask the user which of several accounts sharing an email address to sign in to.  Used by
+ * two flows: OAuth (oauth_pending_* in the cart -> the chosen account is linked to the social
+ * identity) and the passwordless email link (emailLogin_* in the cart -> just sign in). */
 {
 char *provider = cartUsualString(cart, "oauth_pending_provider", "");
-char *email = cartUsualString(cart, "oauth_pending_email", "");
-if (isEmpty(provider) || isEmpty(email))
+boolean emailMode = isEmpty(provider);
+char *email = emailMode ? cartUsualString(cart, "emailLogin_email", "")
+                        : cartUsualString(cart, "oauth_pending_email", "");
+if (isEmpty(email) || (!emailMode && !pendingIdentityValid()))
     {
     displayLoginPage(conn);
     return;
     }
 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() ORDER BY idx",
+        email, email, cartUsualString(cart, "emailLogin_tokenMd5", ""));
+else
     sqlSafef(query, sizeof(query),
         "SELECT * FROM gbMembers WHERE email='%s' ORDER BY idx", email);
 struct gbMembers *list = gbMembersLoadByQuery(conn, query), *m;
 
 hPrintf("<div id=\"chooseAccountBox\" class=\"centeredContainer formBox\">"
     "<h2>%s</h2>", brwName);
 hPrintf("<h3>Choose an account</h3>");
+if (emailMode)
+    hPrintf("<p>The email address <b>%s</b> is associated with more than one %s account. "
+        "Select the account you would like to sign in to.</p>", email, brwName);
+else
     hPrintf("<p>The email address <b>%s</b> 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.</p>",
         email, brwName, oauthProviderLabel(provider));
 hPrintf("<span style='color:red;'>%s</span>", errMsg ? errMsg : "");
 hPrintf("<form method=\"post\" action=\"%s\" name=\"chooseAccountForm\">", hgLoginUrl);
 hPrintf("<div class=\"inputGroup\">");
 boolean first = TRUE;
 for (m = list;  m != NULL;  m = m->next)
     {
     hPrintf("<div class=\"acctHelpSection\">"
-        "<input name=\"hgLogin_chosenUser\" type=\"radio\" value=\"%s\" id=\"acct_%u\"%s>"
+        "<input name=\"hgLogin_chosenIdx\" type=\"radio\" value=\"%u\" id=\"acct_%u\"%s>"
         "<label for=\"acct_%u\" class=\"radioLabel\">%s</label></div>",
-        m->userName, m->idx, first ? " checked" : "", m->idx, m->userName);
+        m->idx, m->idx, first ? " checked" : "", m->idx, m->userName);
     first = FALSE;
     }
 hPrintf("</div>");
 hPrintf("<div class=\"formControls\">"
     "<input type=\"submit\" name=\"hgLogin.do.chooseAccount\" value=\"Sign In\" class=\"largeButton\">"
-    " &nbsp;<a href=\"%s\">Cancel</a>"
+    " &nbsp;<a href=\"%s\" class=\"cancelButton\">Cancel</a>"
     "</div></form></div><!-- END - chooseAccountBox -->", getReturnToURL());
 cartSaveSession(cart);
 gbMembersFreeList(&list);
 }
 
 void chooseAccount(struct sqlConnection *conn)
-/* Link the pending OAuth identity to the account the user picked, then log in. */
+/* Finish the "which account?" chooser: for OAuth, link the pending identity to the chosen
+ * account; for the email link, just sign in.  Either way, only accept an account that really
+ * matches the verified email (and, for the email link, still holds the valid token), never an
+ * arbitrary username the client might submit. */
 {
+int chosenIdx = cartUsualInt(cart, "hgLogin_chosenIdx", 0);
 char *provider = cartUsualString(cart, "oauth_pending_provider", "");
+char query[512];
+
+if (isEmpty(provider))
+    {
+    /* Passwordless email-link mode. */
+    char *email = cartUsualString(cart, "emailLogin_email", "");
+    char *tokenMd5 = cartUsualString(cart, "emailLogin_tokenMd5", "");
+    if (isEmpty(email) || isEmpty(tokenMd5))
+        {
+        freez(&errMsg);
+        errMsg = cloneString("Your login link expired. Please request a new one.");
+        displayLoginPage(conn);
+        return;
+        }
+    sqlSafef(query, sizeof(query),
+        "SELECT * FROM gbMembers WHERE idx=%d AND (email='%s' OR recovEmail='%s') "
+        "AND loginToken='%s' AND loginToken<>'' AND loginTokenExpires > NOW()",
+        chosenIdx, email, email, tokenMd5);
+    struct gbMembers *m = gbMembersLoadByQuery(conn, query);
+    if (m == NULL)
+        {
+        freez(&errMsg);
+        errMsg = cloneString("Please choose one of the listed accounts.");
+        chooseAccountPage(conn);
+        return;
+        }
+    /* Consume the token on every account that shared it (single use), then sign in. */
+    sqlSafef(query, sizeof(query),
+        "UPDATE gbMembers SET loginToken='' WHERE (email='%s' OR recovEmail='%s') AND loginToken='%s'",
+        email, email, tokenMd5);
+    sqlUpdate(conn, query);
+    cartRemove(cart, "emailLogin_email");
+    cartRemove(cart, "emailLogin_tokenMd5");
+    cartRemove(cart, "hgLogin_chosenIdx");
+    loginAndReturn(m->userName, m->idx);
+    gbMembersFree(&m);
+    return;
+    }
+
+/* OAuth mode. */
 char *subject = cartUsualString(cart, "oauth_pending_subject", "");
 char *email = cartUsualString(cart, "oauth_pending_email", "");
-char *chosen = cartUsualString(cart, "hgLogin_chosenUser", "");
-if (isEmpty(provider) || isEmpty(subject) || isEmpty(email))
+if (isEmpty(subject) || isEmpty(email) || !pendingIdentityValid())
     {
     freez(&errMsg);
     errMsg = cloneString("Your login session expired. Please sign in again.");
     displayLoginPage(conn);
     return;
     }
-/* Security: only allow linking to an account that really shares the verified email,
- * not an arbitrary username the client might submit. */
-char query[512];
 sqlSafef(query, sizeof(query),
-    "SELECT * FROM gbMembers WHERE userName='%s' AND email='%s'", chosen, email);
+    "SELECT * FROM gbMembers WHERE idx=%d AND email='%s'", chosenIdx, email);
 struct gbMembers *m = gbMembersLoadByQuery(conn, query);
 if (m == NULL)
     {
     freez(&errMsg);
     errMsg = cloneString("Please choose one of the listed accounts.");
     chooseAccountPage(conn);
     return;
     }
 struct oauthIdentity pending;
 ZeroVar(&pending);
 pending.provider = provider;
 pending.subject = subject;
 pending.email = email;
 linkIdentity(conn, m->idx, &pending);
 clearPendingIdentity();
-cartRemove(cart, "hgLogin_chosenUser");
+cartRemove(cart, "hgLogin_chosenIdx");
 loginAndReturn(m->userName, m->idx);
 gbMembersFree(&m);
 }
 
 static void resolveIdentity(struct sqlConnection *conn, struct oauthIdentity *id)
 /* Log in the user behind an authenticated provider identity:
  *  1. If the provider gave a verified email matching MORE THAN ONE account, always let the
  *     user pick which one -- even if this identity was linked before. Because login cookies
  *     never expire, a user goes through OAuth very rarely, so an occasional pick is cheap
  *     and it lets a person with several same-email accounts choose freely each time.
  *  2. Else if the (provider,subject) is already linked, log into that account.
  *  3. Else if the verified email matches exactly one account, auto-link and log in.
  *  4. Else send the user to the "choose a username" page to finish a new account.
  * (Providers that don't release an email, e.g. ORCID, never reach step 1 or 3 and rely on
  *  the stored link from step 2.) */
@@ -1990,149 +2102,178 @@
 struct oauthIdentity *id = oauthFetchIdentity(provider, code, hgLoginUrl);
 if (id == NULL)
     {
     freez(&errMsg);
     errMsg = cloneString("We could not complete the social login. Please try again.");
     displayLoginPage(conn);
     return;
     }
 resolveIdentity(conn, id);
 oauthIdentityFree(&id);
 }
 
 void emailLinkPage(struct sqlConnection *conn)
 /* Standalone page that asks for an email address and sends a one-time login link. */
 {
+if (!emailLinkEnabled())
+    {
+    displayLoginPage(conn);
+    return;
+    }
 hPrintf("<div id=\"emailLinkBox\" class=\"centeredContainer formBox\">"
     "<h2>%s</h2>", brwName);
-hPrintf("<h3>Sign in with an email link</h3>");
+hPrintf("<h3>Email me a sign-in link</h3>");
 hPrintf("<p>Enter your email address and we'll send you a link that signs you in without a "
     "password. This is handy on a computer where you don't have your password saved.</p>");
 hPrintf("<span style='color:red;'>%s</span>", errMsg ? errMsg : "");
 hPrintf("<form method=\"post\" action=\"%s\" name=\"emailLinkForm\">", hgLoginUrl);
 hPrintf("<div class=\"inputGroup\">"
     "<label for=\"emailLink\">Email address</label>"
     "<input type=\"text\" name=\"hgLogin_email\" value=\"%s\" size=\"30\" id=\"emailLink\">"
     "</div>", cartUsualString(cart, "hgLogin_email", ""));
 hPrintf("<div class=\"formControls\">"
     "<input type=\"submit\" name=\"hgLogin.do.sendEmailLink\" value=\"Send login link\" class=\"largeButton\">"
-    " &nbsp;<a href=\"%s\">Cancel</a>"
+    " &nbsp;<a href=\"%s\" class=\"cancelButton\">Cancel</a>"
     "</div></form></div><!-- END - emailLinkBox -->", getReturnToURL());
 cartSaveSession(cart);
 }
 
 void displayLoginLinkSuccess()
 /* Confirmation shown after a passwordless login link is (possibly) emailed.  Phrased so it
  * does not reveal whether an account exists for the address. */
 {
 char *email = cartUsualString(cart, "hgLogin_sendMailTo", "");
 hPrintf("<div id=\"confirmationBox\" class=\"centeredContainer formBox\">"
     "<h2>%s</h2>", brwName);
 hPrintf("<p id=\"confirmationMsg\" class=\"confirmationTxt\">If an account exists for "
     "<B>%s</B>, a login link has been sent to that address.<BR><BR>"
     "Click the link in that email to sign in &mdash; no password needed. "
     "The link works once and expires in one hour.</p>", email);
 hPrintf("<p>If you don't see the email, please check your spam folder.</p>");
 hPrintf("<p><a href=\"%s?hgLogin.do.displayLoginPage=1\">Return to Login</a></p>\n", hgLoginUrl);
 cartRemove(cart, "hgLogin_email");
 cartRemove(cart, "hgLogin_sendMailTo");
 cartRemove(cart, "hgLogin_helpWith");
 }
 
-void sendLoginLinkMail(char *username, char *email, char *token)
-/* Email a one-time passwordless login link to the user. */
+void sendLoginLinkMail(char *email, char *token)
+/* Email a one-time passwordless login link to an address.  The link identifies the address,
+ * not a single account: if the address has several accounts, the user picks one after
+ * clicking (see emailLogin), so one email covers them all. */
 {
 char subject[256];
 char msg[4096];
 char url[512];
 char *remoteAddr = getenv("REMOTE_ADDR");
-safef(url, sizeof(url), "%s?hgLogin.do.emailLogin=1&user=%s&token=%s",
-    hgLoginUrl, cgiEncode(username), cgiEncode(token));
+safef(url, sizeof(url), "%s?hgLogin.do.emailLogin=1&email=%s&token=%s",
+    hgLoginUrl, cgiEncode(email), cgiEncode(token));
 safef(subject, sizeof(subject), "Your login link for the %s", brwName);
 safef(msg, sizeof(msg),
     "Someone (probably you, from IP address %s) requested a login link for the %s account "
-    "\"%s\".\nClick the link below to sign in without a password. It works once and expires "
-    "in one hour:\n\n%s\n\nIf you did not request this, you can safely ignore this email.\n\n%s\n%s",
-    remoteAddr, brwName, username, url, signature, returnAddr);
+    "registered to this email address.\nClick the link below to sign in without a password. "
+    "It works once and expires in one hour:\n\n%s\n\nIf you did not request this, you can "
+    "safely ignore this email.\n\n%s\n%s",
+    remoteAddr, brwName, url, signature, returnAddr);
 sendActMailOut(email, subject, msg);
 }
 
 void sendEmailLink(struct sqlConnection *conn)
 /* Generate and email a one-time passwordless login link to the address on file. */
 {
+if (!emailLinkEnabled())
+    {
+    displayLoginPage(conn);
+    return;
+    }
 char *email = cartUsualString(cart, "hgLogin_email", "");
 if (isEmpty(email) || spc_email_isvalid(email) == 0)
     {
     freez(&errMsg);
     errMsg = cloneString("Please enter a valid email address.");
     emailLinkPage(conn);
     return;
     }
 char query[512];
 sqlSafef(query, sizeof(query),
     "SELECT * FROM gbMembers WHERE email='%s' OR recovEmail='%s'", email, email);
 struct gbMembers *list = gbMembersLoadByQuery(conn, query), *m;
-for (m = list; m != NULL; m = m->next)
+if (list != NULL)
     {
+    /* One token for the address, stored on every account that uses it, and one email.
+     * The user proves they own the address by clicking; only then (in emailLogin) do we
+     * reveal the accounts and let them choose, so we never disclose accounts to someone
+     * who merely typed the address here. */
     char *token = makeRandomKey(128+33);
     char *tokenMD5 = generateTokenMD5(token);
+    for (m = list; m != NULL; m = m->next)
+        {
         sqlSafef(query, sizeof(query),
             "UPDATE gbMembers SET loginToken='%s', "
             "loginTokenExpires=DATE_ADD(NOW(), INTERVAL 1 HOUR) WHERE idx=%u",
             tokenMD5, m->idx);
         sqlUpdate(conn, query);
-    sendLoginLinkMail(m->userName, email, token);
         }
+    sendLoginLinkMail(email, token);
+    }
+gbMembersFreeList(&list);
 /* Always show the same confirmation, even when no account matched, so we don't reveal
  * whether an address is registered. */
 cartSetString(cart, "hgLogin_sendMailTo", email);
 displayLoginLinkSuccess();
 }
 
 void emailLogin(struct sqlConnection *conn)
-/* Validate a one-time email login token and log the user in. */
+/* Validate a one-time email login token.  The token proves the user owns the address; if it
+ * matches one account, log straight in; if it matches several accounts that share the
+ * address, show the account chooser (the same one the OAuth flow uses). */
+{
+if (!emailLinkEnabled())
     {
-char *user = cgiUsualString("user", "");
+    displayLoginPage(conn);
+    return;
+    }
+char *email = cgiUsualString("email", "");
 char *token = cgiUsualString("token", "");
+char *tokenMD5 = generateTokenMD5(token);
 char query[512];
-sqlSafef(query, sizeof(query), "SELECT * FROM gbMembers WHERE userName='%s'", user);
-struct gbMembers *m = gbMembersLoadByQuery(conn, query);
-if (m == NULL)
+sqlSafef(query, sizeof(query),
+    "SELECT * FROM gbMembers WHERE (email='%s' OR recovEmail='%s') AND loginToken='%s' "
+    "AND loginToken<>'' AND loginTokenExpires > NOW() ORDER BY idx", email, email, tokenMD5);
+struct gbMembers *list = gbMembersLoadByQuery(conn, query);
+int n = slCount(list);
+if (n == 0)
     {
     freez(&errMsg);
     errMsg = cloneString("This login link is invalid or has expired. Please request a new one.");
     displayLoginPage(conn);
-    return;
     }
-char *tokenMD5 = generateTokenMD5(token);
-sqlSafef(query, sizeof(query),
-    "SELECT count(*) FROM gbMembers WHERE userName='%s' AND loginToken='%s' "
-    "AND loginToken<>'' AND loginTokenExpires > NOW()", user, tokenMD5);
-if (sqlQuickNum(conn, query) == 1)
+else if (n == 1)
     {
     sqlSafef(query, sizeof(query),
-        "UPDATE gbMembers SET loginToken='', lastUse=NOW() WHERE userName='%s'", user);
+        "UPDATE gbMembers SET loginToken='', lastUse=NOW() WHERE idx=%u", list->idx);
     sqlUpdate(conn, query);
-    loginAndReturn(m->userName, m->idx);
+    loginAndReturn(list->userName, list->idx);
     }
 else
     {
-    freez(&errMsg);
-    errMsg = cloneString("This login link is invalid or has expired. Please request a new one.");
-    displayLoginPage(conn);
+    /* Several accounts share this now-verified address: stash the proof and let the user
+     * pick one.  chooseAccount re-checks the token before logging in. */
+    cartSetString(cart, "emailLogin_email", email);
+    cartSetString(cart, "emailLogin_tokenMd5", tokenMD5);
+    chooseAccountPage(conn);
     }
-gbMembersFree(&m);
+gbMembersFreeList(&list);
 }
 
 void doMiddle(struct cart *theCart)
 /* Write the middle parts of the HTML page.
  * This routine sets up some globals and then
  * dispatches to the appropriate page-maker. */
 {
 struct sqlConnection *conn = hConnectCentral();
 
 // on mirrors, try to add the field 'recovEmail' to gbMembers. This may or may not work, depending on their config
 if (sqlFieldIndex(conn, "gbMembers", "recovEmail") == -1) {
     autoUpgradeTableAddColumn(conn, "gbMembers", "recovEmail", "varchar(255)", FALSE, "''");
 }
 
 // columns for the passwordless email-link login feature
@@ -2209,29 +2350,31 @@
 errAbort(
   "hgLogin - Stand alone CGI to handle Genome Browser login.\n"
   "usage:\n"
   "    hgLogin <various CGI settings>\n"
   );
 }
 
 int main(int argc, char *argv[])
 /* Process command line. */
 {
 
 long enteredMainTime = clock1000();
 earlyBotCheck(enteredMainTime, "hgLogin", delayFraction, 0, 0, "html");
 pushCarefulMemHandler(100000000);
 cgiSpoof(&argc, argv);
-htmlSetStyleSheet("../style/userAccounts.css");
+/* Use the site's standard time-stamped resource link (appends ?v=<mtime>) so browsers pick
+ * up CSS changes after a release instead of serving a stale cached copy. */
+htmlSetStyleSheet(webTimeStampedLinkToResource("userAccounts.css", FALSE));
 htmlSetStyle(htmlStyleUndecoratedLink);
 htmlSetBgColor(HG_CL_OUTSIDE);
 htmlSetFormClass("accountScreen");
 
 struct dyString *dy;
 dy = dyStringCreate("%shgLogin", hLoginHostCgiBinUrl());
 hgLoginUrl = dyStringCannibalize(&dy);
 
 oldCart = hashNew(10);
 cartHtmlShell("Login - UCSC Genome Browser", doMiddle, hUserCookie(), excludeVars, oldCart);
 cgiExitTime("hgLogin", enteredMainTime);
 return 0;
 }