83dd847b7449dd0fa83c22a4d60854f8d20d2847
max
  Fri Sep 18 08:51:14 2026 -0700
hgLogin: stop telling a CILogon user that CILogon shares no email address

#Preview2 week - bugs introduced now will need a build patch to fix
QA found three messages on the social sign-in pages claiming the provider does
not give us an address.  That was written when ORCID, which really releases
none, was the only provider reaching those pages.  CILogon does release the
institution's address; we drop it because CILogon never marks it verified and
login.oauth.cilogon.trustEmail is what says we may take it anyway.  Once the
address is dropped, nothing downstream could tell "released nothing" from
"released something we would not take".

oauthFetchIdentity now keeps the dropped address in a separate field that
matching and sign-in never look at, and the three messages read it: the choose
a username page, the confirmation page, and the one that turns down a typed
address that already belongs to an account.  The address box on the choose a
username page starts from it as well, since retyping what we were just handed
is the last thing a person wants to do.  It still has to clear the duplicate
check and be confirmed by mail, exactly as a typed address does.

Three more things on the confirmation page, all from the same QA pass:

The generic "a confirmation email has been sent" paragraph printed below the
"use this address instead" form, so it read as if a mail had already gone to
whatever was about to be typed in.  It now goes above the form.

Correcting the address redisplayed the same page with the new address swapped
in and nothing to say the change had worked.  It now says so.  The rejection
reason for an address that is already in use was set but never printed, so a
refused change looked like nothing happening at all; that is printed now too.

Reworded the unconfirmed-account explanation as QA suggested.

refs #38339

diff --git src/hg/hgLogin/oauthLogin.c src/hg/hgLogin/oauthLogin.c
index 2c7a625f20f..131c7e03245 100644
--- src/hg/hgLogin/oauthLogin.c
+++ src/hg/hgLogin/oauthLogin.c
@@ -516,38 +516,44 @@
     else
         id = oidcFetch(p, code, redirectUri);
     }
 errCatchEnd(errCatch);
 if (errCatch->gotError)
     {
     fprintf(stderr, "hgLogin oauth: identity fetch for %s failed: %s\n", name, errCatch->message->string);
     id = NULL;
     }
 errCatchFree(&errCatch);
 /* An address the provider did not mark verified is only a string it is holding for the user,
  * and the user may well have typed it themselves.  Unless hg.conf vouches for this provider,
  * forget it and let hgLogin treat the sign-in as one that came with no address at all: it then
  * asks for one and confirms it by mail, the same as ORCID, which releases none.  Dropping it
  * rather than flagging it also keeps it out of the account matching in resolveIdentity, where
- * an unverified address would otherwise be enough to reach somebody else's account. */
+ * an unverified address would otherwise be enough to reach somebody else's account.
+ * Keep the address itself in emailUnverified, though.  "Released nothing" and "released
+ * something we will not take" look identical downstream once email is NULL, and they are not
+ * the same thing to tell a user: telling a CILogon user that CILogon shares no address is
+ * simply untrue (#38339). */
 if ((id != NULL) && isNotEmpty(id->email) && !id->emailVerified && !p->trustEmail)
     {
     fprintf(stderr, "hgLogin oauth: %s did not verify the address it released; asking the user "
         "for one instead (set login.oauth.%s.trustEmail=on to accept it)\n", name, name);
-    freez(&id->email);
+    id->emailUnverified = id->email;
+    id->email = NULL;
     }
 return id;
 }
 
 void oauthIdentityFree(struct oauthIdentity **pId)
 /* Free an oauthIdentity. */
 {
 struct oauthIdentity *id = *pId;
 if (id != NULL)
     {
     freeMem(id->provider);
     freeMem(id->subject);
     freeMem(id->email);
+    freeMem(id->emailUnverified);
     freeMem(id->displayName);
     freez(pId);
     }
 }