Skip to content

auth: use OIDC sub claim as stable account key, not email #504

@Neelagiri65

Description

@Neelagiri65

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

  1. The keyring entry under old@gmail.com is orphaned. gog auth add new@gmail.com creates a second entry, not an update.
  2. account_aliases and account_clients entries for the old email stay behind unless the user remembers to gog auth remove old@gmail.com first.
  3. Scripts and agent invocations hardcoding the old email silently target a stale or missing account.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions