f85553903a3f87b3029f94e49f0a7d1bb805445b max Mon Aug 3 12:52:33 2026 -0700 hgLogin: configurable OIDC providers, GitHub login, top-level email-link button, sign-in wording. refs #37984 diff --git src/hg/hgLogin/hgLogin.c src/hg/hgLogin/hgLogin.c index fdb8ee643e7..f7cc0fdc79f 100644 --- src/hg/hgLogin/hgLogin.c +++ src/hg/hgLogin/hgLogin.c @@ -52,32 +52,34 @@ 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(); +static void printSocialButtons(boolean dividerAbove, boolean dividerBelow); +static void printEmailLinkButton(); 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)); } @@ -797,31 +799,32 @@ "" "" "\n" "
" " " "  Cancel" "
" , getReturnToURL()); if (pwdEyeIconEnabled) { printPwdToggleJS(); jsOnEventById("click", "pwdEyeIcon", "togglePwdVisibility('password','pwdEyeSlash');"); } cartSaveSession(cart); hPrintf("\n"); -printSocialButtons(); +printEmailLinkButton(); +printSocialButtons(TRUE, FALSE); hPrintf( "
" "Forgot username, password or sign in with an email link
" "Need an account? Sign up.
" "
" "" "\n" "\n" "" "", hgLoginUrl, hgLoginUrl); } void activateAccount(struct sqlConnection *conn) /* activate account */ { @@ -1084,34 +1087,36 @@ cartRemove(cart, "hgLogin_newEmail1"); cartRemove(cart, "hgLogin_newEmail2"); hPrintf("

%s

", brwName); hPrintf("

Your email address has been changed.

"); hPrintf("

Your email address is now %s.

", email1); returnToURL(1500); } void signupPage(struct sqlConnection *conn) /* draw the signup page */ { hPrintf("
" "

%s

", brwName); hPrintf( "

Signing up enables you to save multiple sessions and to share your sessions with others.

" - "Already have an account? Login.
" - "\n", hgLoginUrl); -printSocialButtons(); -hPrintf("

Sign Up

" + "\n"); +hPrintf("" + "Sign in with username and password", hgLoginUrl); +printEmailLinkButton(); +printSocialButtons(TRUE, TRUE); +hPrintf("

Sign Up Using Email

" "
" "%s" "\n", hgLoginUrl, errMsg ? errMsg : ""); printUsernameNote(); hPrintf("
" "" "" "
" "\n" "
" "" "" "
" "\n" "
" @@ -1138,31 +1143,31 @@ "" "
" "\n" "
" "" "" "", cartUsualString(cart, "hgLogin_password2", "")); printPwdEyeIcon("signupPwCheckEyeIcon", "signupPwCheckEyeSlash"); hPrintf( "" "\n" "
" "\n" "
" - "   " + "   " " Cancel" "
" "
" "
", getReturnToURL()); if (pwdEyeIconEnabled) { printPwdToggleJS(); jsOnEventById("click", "signupPwEyeIcon", "togglePwdVisibility('password','signupPwEyeSlash');"); jsOnEventById("click", "signupPwCheckEyeIcon", "togglePwdVisibility('passwordCheck','signupPwCheckEyeSlash');"); } cartSaveSession(cart); } void signup(struct sqlConnection *conn) @@ -1500,47 +1505,60 @@ "

" "

" "" "\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() -/* Print sign-in buttons for any enabled social login providers. Prints nothing if no +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. */ { if (!oauthAnyProviderEnabled()) return; -hPrintf("
" - "
or
"); -if (oauthProviderEnabled(OAUTH_PROVIDER_GOOGLE)) - hPrintf("" - "Sign in with Google", hgLoginUrl); -if (oauthProviderEnabled(OAUTH_PROVIDER_ORCID)) - hPrintf("" - "Sign in with ORCID", hgLoginUrl); +hPrintf("
"); +if (dividerAbove) + hPrintf("
or
"); +struct slName *prov, *providers = oauthProviderNames(); +for (prov = providers; prov != NULL; prov = prov->next) + hPrintf("" + "Sign in with %s", + hgLoginUrl, cgiEncode(prov->name), oauthProviderLabel(prov->name)); +if (dividerBelow) + hPrintf("
or
"); hPrintf("
"); } +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). */ +{ +hPrintf("" + "Sign in with an email link", 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("

Note: your username becomes part of every short link " "you create later (for example %s/s/username/MySession), so choose " "something short and easy to type.

", 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. */ { hPrintf("

%s

", brwName); @@ -1552,31 +1570,31 @@ } static void createIdentityTable(struct sqlConnection *conn) /* Create the gbMemberIdentity table if it does not exist. On a mirror whose central db * is read-only this may fail; social login simply won't work there (and won't be enabled * without client secrets anyway), so ignore any error. */ { if (sqlTableExists(conn, "gbMemberIdentity")) return; struct errCatch *errCatch = errCatchNew(); if (errCatchStart(errCatch)) { struct dyString *dy = sqlDyStringCreate( "CREATE TABLE IF NOT EXISTS gbMemberIdentity (" "idx int unsigned NOT NULL," - "provider varchar(16) NOT NULL," + "provider varchar(64) NOT NULL," "subject varchar(255) NOT NULL," "email varchar(255) NOT NULL default ''," "created DATETIME NOT NULL," "lastUse DATETIME NOT NULL," "UNIQUE KEY provSub (provider, subject)," "INDEX(idx))"); sqlUpdate(conn, dyStringContents(dy)); dyStringFree(&dy); } errCatchEnd(errCatch); errCatchFree(&errCatch); } static boolean userNameTaken(struct sqlConnection *conn, char *userName) /* Return TRUE if userName already exists in gbMembers. */ @@ -1671,31 +1689,31 @@ char *name = cartUsualString(cart, "oauth_pending_name", ""); if (isEmpty(provider)) { displayLoginPage(conn); return; } char *suggested = cartUsualString(cart, "hgLogin_userName", ""); if (isEmpty(suggested)) suggested = suggestUsername(conn, email, name); hPrintf("
" "

%s

", brwName); hPrintf("

Choose a username

"); hPrintf("

You signed in with %s. Pick a username for your new %s account. " "You can change the suggested name below.

", - sameString(provider, OAUTH_PROVIDER_ORCID) ? "ORCID" : "Google", brwName); + oauthProviderLabel(provider), brwName); printUsernameNote(); hPrintf("%s", errMsg ? errMsg : ""); hPrintf("
", hgLoginUrl); hPrintf("
" "" "" "
", suggested); hPrintf("
" "" "" "
", email); hPrintf("
" "" "  Cancel" "
", getReturnToURL()); @@ -1790,31 +1808,31 @@ if (isEmpty(provider) || isEmpty(email)) { displayLoginPage(conn); return; } char query[512]; sqlSafef(query, sizeof(query), "SELECT * FROM gbMembers WHERE email='%s' ORDER BY idx", email); struct gbMembers *list = gbMembersLoadByQuery(conn, query), *m; hPrintf("
" "

%s

", brwName); hPrintf("

Choose an account

"); 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.

", - email, brwName, sameString(provider, OAUTH_PROVIDER_ORCID) ? "ORCID" : "Google"); + email, brwName, oauthProviderLabel(provider)); hPrintf("%s", errMsg ? errMsg : ""); hPrintf("
", hgLoginUrl); hPrintf("
"); boolean first = TRUE; for (m = list; m != NULL; m = m->next) { hPrintf("
" "" "
", m->userName, m->idx, first ? " checked" : "", m->idx, m->userName); first = FALSE; } hPrintf("
"); hPrintf("
" "" @@ -1969,30 +1987,51 @@ return; } char *code = cgiUsualString("code", ""); 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. */ +{ +hPrintf("
" + "

%s

", brwName); +hPrintf("

Sign in with an email link

"); +hPrintf("

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.

"); +hPrintf("%s", errMsg ? errMsg : ""); +hPrintf("", hgLoginUrl); +hPrintf("
" + "" + "" + "
", cartUsualString(cart, "hgLogin_email", "")); +hPrintf("
" + "" + "  Cancel" + "
", 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("
" "

%s

", brwName); hPrintf("

If an account exists for " "%s, a login link has been sent to that address.

" "Click the link in that email to sign in — no password needed. " "The link works once and expires in one hour.

", email); hPrintf("

If you don't see the email, please check your spam folder.

"); hPrintf("

Return to Login

\n", hgLoginUrl); cartRemove(cart, "hgLogin_email"); cartRemove(cart, "hgLogin_sendMailTo"); @@ -2013,31 +2052,31 @@ "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); sendActMailOut(email, subject, msg); } void sendEmailLink(struct sqlConnection *conn) /* Generate and email a one-time passwordless login link to the address on file. */ { char *email = cartUsualString(cart, "hgLogin_email", ""); if (isEmpty(email) || spc_email_isvalid(email) == 0) { freez(&errMsg); errMsg = cloneString("Please enter a valid email address."); - displayAccHelpPage(conn); + 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) { char *token = makeRandomKey(128+33); char *tokenMD5 = generateTokenMD5(token); 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); @@ -2111,30 +2150,32 @@ safecpy(brwAddr,sizeof(brwAddr), browserAddr()); safecpy(signature,sizeof(signature), mailSignature()); safecpy(returnAddr,sizeof(returnAddr), mailReturnAddr()); pwdEyeIconEnabled = cfgOptionBooleanDefault(CFG_LOGIN_PWD_EYE_ICON, TRUE); // A provider's OAuth redirect back to us carries 'code' and 'state' but none of our own // hgLogin.do.* variables, so detect it up front. if (cgiOptionalString("code") != NULL && cgiOptionalString("state") != NULL) oauthReturn(conn); else if (cartVarExists(cart, "hgLogin.do.oauthStart")) oauthStart(conn); else if (cartVarExists(cart, "hgLogin.do.completeAccount")) completeAccount(conn); else if (cartVarExists(cart, "hgLogin.do.chooseAccount")) chooseAccount(conn); +else if (cartVarExists(cart, "hgLogin.do.emailLinkPage")) + emailLinkPage(conn); else if (cartVarExists(cart, "hgLogin.do.sendEmailLink")) sendEmailLink(conn); else if (cartVarExists(cart, "hgLogin.do.emailLogin")) emailLogin(conn); else if (cartVarExists(cart, "hgLogin.do.changePasswordPage")) changePasswordPage(conn); else if (cartVarExists(cart, "hgLogin.do.changePassword")) changePassword(conn); else if (cartVarExists(cart, "hgLogin.do.changeEmailPage")) changeEmailPage(conn); else if (cartVarExists(cart, "hgLogin.do.changeEmail")) changeEmail(conn); else if (cartVarExists(cart, "hgLogin.do.displayAccHelpPage")) displayAccHelpPage(conn); else if (cartVarExists(cart, "hgLogin.do.accountHelp"))