When a user renames their Gmail address (Google enabled this on 31 March 2026), gogcli loses the link to the existing local account. Every data structure in the auth layer keys on email. The OIDC spec calls email mutable and specifies sub as the stable identifier.
What happens today
internal/googleauth/accounts_server.go:585 parses the ID token but reads only email:
var claims struct {
Email string `json:"email"`
}
Downstream, email is the primary key throughout:
internal/config/config.go:17: AccountAliases map[string]string keys on email
internal/config/config.go:18: AccountClients map[string]string keys on email
internal/cmd/auth_add.go:251: normalizeEmail(authorizedEmail) != normalizeEmail(c.Email) rejects the token when the authorised email doesn't match the CLI arg
internal/cmd/auth_add.go:265: keyring entry stored as store.SetToken(client, authorizedEmail, …)
secrets.Token has an Email field, no Sub field
What breaks on rename
- The keyring entry under
old@gmail.com is orphaned. gog auth add new@gmail.com creates a second entry, not an update.
account_aliases and account_clients entries for the old email stay behind unless the user remembers to gog auth remove old@gmail.com first.
- Scripts and agent invocations hardcoding the old email silently target a stale or missing account.
- The line 251 check surfaces as
authorized as X, expected Y rather than "looks like a rename, migrate?".
Minimal fix
Add sub to the claim struct, store it in secrets.Token, use sub as the canonical key in keyring and config. Email becomes a display label.
var claims struct {
Sub string `json:"sub"`
Email string `json:"email"`
}
When a refresh returns an ID token whose sub matches an existing stored account but whose email differs, treat it as a rename: migrate the keyring entry, update account_aliases and account_clients to the new email, log a one-line notice. Existing email-keyed configs need a one-shot migration on first run after upgrade (detect by presence of Email without Sub, re-fetch the ID token, backfill).
The OIDC spec: "The sub Claim ... is never reassigned to another End-User." Google's identity docs call out sub as the stable ID: https://developers.google.com/identity/openid-connect/openid-connect#authenticationuriparameters
Prior art
ztnet landed the equivalent fix two days ago: sinamics/ztnet#884. Their sign-in callback now looks up by provider+subject first, email second. Worth a read as a reference pattern; the Go translation is cleaner because you already have client as the per-account namespace.
Detection
The pattern is one of several flagged by authdrift, a Semgrep ruleset I wrote while auditing 2M+ repos on Sourcegraph for this specific bug. Breakdown of findings: https://neelagiri65.github.io/gmail-oauth-research/. 124 affected repos across Passport.js, NextAuth, Python/Django and Ruby/OmniAuth. Go isn't covered yet because the scan targeted web frameworks, not CLIs. gogcli is the first Go entry I've seen with this exact shape.
I can open a PR if this direction works.
When a user renames their Gmail address (Google enabled this on 31 March 2026), gogcli loses the link to the existing local account. Every data structure in the auth layer keys on email. The OIDC spec calls email mutable and specifies
subas the stable identifier.What happens today
internal/googleauth/accounts_server.go:585parses the ID token but reads onlyemail:Downstream, email is the primary key throughout:
internal/config/config.go:17:AccountAliases map[string]stringkeys on emailinternal/config/config.go:18:AccountClients map[string]stringkeys on emailinternal/cmd/auth_add.go:251:normalizeEmail(authorizedEmail) != normalizeEmail(c.Email)rejects the token when the authorised email doesn't match the CLI arginternal/cmd/auth_add.go:265: keyring entry stored asstore.SetToken(client, authorizedEmail, …)secrets.Tokenhas anEmailfield, noSubfieldWhat breaks on rename
old@gmail.comis orphaned.gog auth add new@gmail.comcreates a second entry, not an update.account_aliasesandaccount_clientsentries for the old email stay behind unless the user remembers togog auth remove old@gmail.comfirst.authorized as X, expected Yrather than "looks like a rename, migrate?".Minimal fix
Add
subto the claim struct, store it insecrets.Token, use sub as the canonical key in keyring and config. Email becomes a display label.When a refresh returns an ID token whose
submatches an existing stored account but whoseemaildiffers, treat it as a rename: migrate the keyring entry, updateaccount_aliasesandaccount_clientsto the new email, log a one-line notice. Existing email-keyed configs need a one-shot migration on first run after upgrade (detect by presence of Email without Sub, re-fetch the ID token, backfill).The OIDC spec: "The sub Claim ... is never reassigned to another End-User." Google's identity docs call out
subas the stable ID: https://developers.google.com/identity/openid-connect/openid-connect#authenticationuriparametersPrior art
ztnet landed the equivalent fix two days ago: sinamics/ztnet#884. Their sign-in callback now looks up by provider+subject first, email second. Worth a read as a reference pattern; the Go translation is cleaner because you already have
clientas the per-account namespace.Detection
The pattern is one of several flagged by authdrift, a Semgrep ruleset I wrote while auditing 2M+ repos on Sourcegraph for this specific bug. Breakdown of findings: https://neelagiri65.github.io/gmail-oauth-research/. 124 affected repos across Passport.js, NextAuth, Python/Django and Ruby/OmniAuth. Go isn't covered yet because the scan targeted web frameworks, not CLIs. gogcli is the first Go entry I've seen with this exact shape.
I can open a PR if this direction works.