Skip to content

fix(oauth): purge v2 consent rows on delete, log dual-write failures - #20972

Open
nshirley wants to merge 1 commit into
mainfrom
account-authz-v2-signal
Open

fix(oauth): purge v2 consent rows on delete, log dual-write failures#20972
nshirley wants to merge 1 commit into
mainfrom
account-authz-v2-signal

Conversation

@nshirley

@nshirley nshirley commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Because

  • Account deletion cleared accountAuthorizations but left the accountAuthorizations_v2 mirror behind, so consent records for deleted accounts persist indefinitely. Once v1 is dropped in Phase 4, those orphans become deleted users' consent living in the only remaining table.
  • The v2 dual-write swallowed every failure and silently skipped scopes it couldn't resolve, so a healthy dual-write and a completely broken one look identical from the outside.

This pull request

  • Adds a DELETE FROM accountAuthorizations_v2 to _deleteAllAccountConsentsForUser in packages/fxa-auth-server/lib/oauth/db/mysql/index.js. It is deliberately not gated on dualWriteV2, since turning that flag off stops new v2 writes but leaves existing rows needing purged, and it runs before the v1 delete so a partial failure can't leave readV2 answering for an account whose v1 rows are already gone.
  • Logs accountAuthorizations.v2.missing_scope (warn) when the scope cache can't resolve a scope, carrying the unresolved strings as a seed list for the scopes table. The array is capped because scope strings are caller-supplied.
  • Logs accountAuthorizations.v2.write_failed (error) when the v2 mirror throws. The write is still swallowed so it can never fail a grant. Only the driver code/errno is logged, not the error, since the mysql driver decorates errors with connection options that can include credentials.
  • Adds a test asserting account deletion clears both tables, and corrects an existing test that relied on deleteAllConsentsForUser dropping v1 only.

Issue that this pull request solves

Closes: N/A

Checklist

Put an x in the boxes that apply

  • My commit is GPG signed.
  • If applicable, I have modified or added tests which pass locally.
  • I have added necessary documentation (if appropriate).
  • I have verified that my changes render correctly in RTL (if appropriate).
  • I have manually reviewed all AI generated code.

How to review (Optional)

  • Key files/areas to focus on:
  • Suggested review order:
  • Risky or complex parts:

Screenshots (Optional)

Please attach the screenshots of the changes made in case of change in user interface.

Other information (Optional)

Integration tests were not run locally — the local MySQL container is missing the fxa_oauth schema, so every suite touching it fails at connection time. Lint passes; relying on CI to run test/remote/account_consents.in.spec.ts.

}
} catch {
// v2 is best-effort; v1 already landed. Intentionally swallowed.
} catch (err) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thankfully, once the v2 table is backfilled and fully working in prod we can come back and rip out a lot of the code complexity here. This is just logging while v1 and v2 are running so we can make sure it's working as expected

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a cleanup ticket that we can reference so this isn't missed? Is there a ticket we can't reference to make sure this gets cleaned up when no longer needed?

@nshirley
nshirley marked this pull request as ready for review August 5, 2026 23:28
@nshirley
nshirley requested a review from a team as a code owner August 5, 2026 23:28
Copilot AI review requested due to automatic review settings August 5, 2026 23:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the OAuth account-consent v2 migration by ensuring account deletion purges the v2 mirror table and by adding logging so dual-write problems (missing scopes / v2 write failures) are observable during the bake period.

Changes:

  • Delete accountAuthorizations_v2 rows during _deleteAllAccountConsentsForUser (v2-first, then v1).
  • Add warning/error logs for v2 dual-write missing-scope resolution and v2 write failures (without logging full DB errors).
  • Update/add integration tests to reflect the new “delete clears both tables” behavior and to assert v2 rows are removed on deletion.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
packages/fxa-auth-server/lib/oauth/db/mysql/index.js Adds v2 consent purge on delete and logs dual-write missing scopes / write failures.
packages/fxa-auth-server/test/remote/account_consents.in.spec.ts Updates an existing v2-read test to delete v1 directly, and adds a test ensuring deletion clears v2 rows.

Comment on lines +669 to +672
this.log?.warn('accountAuthorizations.v2.missing_scope', {
scopes: missing.slice(0, 10),
count: missing.length,
});

@vpomerleau vpomerleau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few small comments, take them or leave them!

}
} catch {
// v2 is best-effort; v1 already landed. Intentionally swallowed.
} catch (err) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a cleanup ticket that we can reference so this isn't missed? Is there a ticket we can't reference to make sure this gets cleaned up when no longer needed?

// Drop the v1 rows only; the v2 row remains, so a v2 read is the only
// thing that can still find this consent.
await db.deleteAllConsentsForUser(id);
// thing that can still find this consent. Deletes both tables now, so this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No sure I understand the comment here - this seems to explain why we aren't using deleteAllConsentsForUser without naming it?

expect(await db.hasConsentForSignIn(id, UNKNOWN_SCOPE, '')).toBe(true);
});

it('account deletion clears the v2 row, not just v1', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should this be account deletion clears both v1 and v2 rows, + add a check for v1 (maybe it's there but not as clearly asserted as v2 checks)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants