Files
accounted/tests/pg
Mattsson 65bd675f43 fix(auth): unlink social identities bound to the old address when the login email changes (#2208)
* fix(auth): unlink social identities bound to the old address when the login email changes

GoTrue keys OAuth identities on the provider subject, so after a secure
email change from A to B the Google identity auto-linked for A stayed on
the account and "Logga in med Google" from the A mailbox still opened the
company (prod 2026-09-03, willemduplessis999 -> levandefisken kept both
Google logins). A change is a change: only identities bound to the
address the user switched from go; the email identity, password, BankID
and social identities on other addresses stay, and Google with the new
address re-links itself on the first sign-in.

Migration 20260903110000 adds a BEFORE UPDATE OF email trigger on
auth.users (next to sync_profile_email) that deletes those identities and
recomputes app_metadata.providers. A trigger covers every completion
path: hook link, stock link, phone click without a session, admin-side
change. pg-real test covers removal, keep-others, case-insensitive match,
email identity untouched, no-op on unchanged email, and other users on
the same address. Applied to staging under the same version.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LMFybWJqw8vScQiEDwKXGi

* fix(auth): make the old-identity unlink trigger safe without GoTrue and keep Google-only accounts reachable

Skeptic findings on 5889aec7e:

- The pg-real container has no auth.identities (GoTrue creates it and
  does not run in CI), so the trigger failed every auth.users email
  update there, including the existing profile-email-sync suite. Guard the
  function with to_regclass and bootstrap a GoTrue-shaped auth.identities
  in tests/pg/bootstrap.sql so the trigger's own tests actually run.
- A Google-only account (no password, no email identity) ended with zero
  identities after the change, and whether Google with the new address
  re-links then depends on GoTrue internals. When the trigger removes the
  last social identity and no email identity exists, it now creates the
  email identity for the new address, the row GoTrue links Google through
  and password recovery resolves. pg-real tests cover both cases.
- Self-hosting note: keep secure email change enabled, since a change now
  also removes the old address's social logins.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LMFybWJqw8vScQiEDwKXGi

* fix(auth): verified flag, audit trail and redaction for the old-identity unlink trigger

Second review round on PR #2208:

- Superagent P1: the synthesized email identity claimed email_verified
  for every email update, admin-side included. It is now verified only
  when the pending address became the address in the same write (the
  signature of GoTrue's ConfirmEmailChange); anything else gets an
  unverified identity, as GoTrue itself would create it.
- Compliance swarm A.8.15: removing a login method left no trail. The
  trigger now writes an identity_unlink entry to auth.audit_log_entries
  with the removed providers, old and new address and whether the change
  was confirmed, next to GoTrue's own user_modified entry.
- Compliance swarm A.5.34: the migration comment named real test accounts;
  redacted.
- pg-real: the cross-user test still expected the old-address user to end
  with zero identities; it now expects the email identity the previous
  round introduced. New tests cover the unverified admin path and the
  audit entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LMFybWJqw8vScQiEDwKXGi

* test(pg): give the CI auth audit table the ip_address column GoTrue adds

pg-real runs against the bare Postgres image, whose auth.audit_log_entries
predates GoTrue's ip_address column (NOT NULL DEFAULT '' on every hosted
project). unlink_old_address_identities writes that column, so all seven
trigger tests failed with 42703 in CI while the same migration ran clean
on staging. Mirror the real shape in the bootstrap instead of changing a
migration that is already applied under this version.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 14:25:32 +02:00
..