Skip to content

fix(studio): await MFA factor deletions before responding - #48942

Open
SEPURI-SAI-KRISHNA wants to merge 1 commit into
supabase:masterfrom
SEPURI-SAI-KRISHNA:fix/self-hosted-mfa-factor-deletion
Open

fix(studio): await MFA factor deletions before responding#48942
SEPURI-SAI-KRISHNA wants to merge 1 commit into
supabase:masterfrom
SEPURI-SAI-KRISHNA:fix/self-hosted-mfa-factor-deletion

Conversation

@SEPURI-SAI-KRISHNA

@SEPURI-SAI-KRISHNA SEPURI-SAI-KRISHNA commented Aug 11, 2026

Copy link
Copy Markdown

I have read the CONTRIBUTING.md file.

YES

What kind of change does this PR introduce?

Bug fix.

What is the current behavior?

On self-hosted Studio, Authentication → Users → (user) → Remove MFA factors can report success without having removed anything.

apps/studio/pages/api/platform/auth/[ref]/users/[id]/factors.ts deletes the factors with an unawaited forEach(async …):

factors?.factors.forEach(async (factor: any) => {
  const { error } = await supabase.auth.admin.mfa.deleteFactor({ id: factor.id, userId: id as string })
  if (error) {
    return res.status(400).json({ error: { message: error.message } })
  }
})

return res.status(200).json({ data: null, error: null })

forEach does not await its callback, so the handler falls straight through to the 200 while the deleteFactor calls are still in flight. Two consequences:

  1. Failures are never surfaced. The 200 has already been sent by the time a factor's error is inspected, so useUserDeleteMFAFactorsMutation always lands in onSuccess and UserOverview.tsx shows "Successfully deleted the user's factors" — even when every deletion failed and the user is still fully enrolled. For an admin unenrolling a locked-out or compromised user's MFA, that is a misleading confirmation of a security-relevant action.
  2. The response body gets corrupted. The return res.status(400).json(...) inside the callback runs after the response was already sent, so the 400 payload is appended to the 200 payload. The body becomes two concatenated JSON documents ({"data":null,"error":null}{"error":{"message":"…"}}), and the write-after-end raises ERR_HTTP_HEADERS_SENT in the server logs.

This is reachable only on self-hosted deployments — pages/api/platform/** are the self-hosted shims, since API_URL only falls back to /api when IS_PLATFORM is false.

The second failing test below is what surfaced the body corruption: it fails with SyntaxError: Unexpected non-whitespace character after JSON at position 26, position 26 being the exact length of the already-sent 200 body.

What is the new behavior?

The handler now awaits all deletions before responding, and returns a 400 carrying the first deletion error:

const deletionErrors = await Promise.all(
  (factors?.factors ?? []).map(async (factor) => {
    const { error } = await supabase.auth.admin.mfa.deleteFactor({ id: factor.id, userId: id as string })
    return error
  })
)

const failedDeletion = deletionErrors.find((deletionError) => !!deletionError)
if (failedDeletion) {
  return res.status(400).json({ error: { message: failedDeletion.message } })
}
  • The response is sent only once, after every deletion has settled, so the success toast now means the factors are actually gone.
  • Every factor is still attempted even if one fails (.map starts them all concurrently), so a single bad factor doesn't leave the rest enrolled.
  • The 400 shape matches the sibling self-hosted auth routes (users/[id]/index.ts).
  • Drops the factor: anylistFactors already types the factors, which also lowers the @typescript-eslint/no-explicit-any ratchet count by one.

Adds apps/studio/tests/pages/api/platform/auth/[ref]/users/[id]/factors.test.ts (7 tests), following the existing tests/pages/api/** + node-mocks-http pattern. Two of them fail against the old handler:

  • waits for the deletions to finish before responding — holds deleteFactor on a deferred promise and asserts the response is still open.
  • returns a 400 when a factor fails to delete — the old handler returns the corrupted double-JSON body.

Additional context

Checks run locally:

  • pnpm --filter studio exec vitest run tests/pages/api → 37 passed (5 files)
  • pnpm --filter studio run typecheck → clean
  • pnpm --filter studio run lint:ratchet → "Nice! Some rules improved."
  • npx prettier --check on both changed files → clean

No API contract change: success and failure response shapes are unchanged, only when they are sent and whether failures are reported.

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability when removing a user’s multi-factor authentication (MFA) factors.
    • Deletion requests now wait for all factors to be processed before returning a result.
    • Clearer error responses are provided when one or more factor deletions fail.
    • Remaining factors continue to be processed even if an individual deletion encounters an error.
  • Tests

    • Added coverage for successful deletions, partial failures, missing factors, listing errors, and unsupported request methods.

@SEPURI-SAI-KRISHNA
SEPURI-SAI-KRISHNA requested a review from a team as a code owner August 11, 2026 14:35
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

@SEPURI-SAI-KRISHNA is attempting to deploy a commit to the Supabase Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for contributing to Supabase! ❤️ Our team will review your PR.

A few tips for a smoother review process:

  • If you have a local version of the repo, run pnpm run format to make sure formatting checks pass.
  • Once we've reviewed your PR, please don't trivially merge master (don't click Update branch if there are no merge conflicts to be fixed). This invalidates any pre-merge checks we've run.

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
design-system Ready Ready Preview Aug 11, 2026 2:36pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d25cdc0-f590-47f7-a9d6-cbb48587a6d7

📥 Commits

Reviewing files that changed from the base of the PR and between e846d45 and 8793062.

📒 Files selected for processing (2)
  • apps/studio/pages/api/platform/auth/[ref]/users/[id]/factors.ts
  • apps/studio/tests/pages/api/platform/auth/[ref]/users/[id]/factors.test.ts

📝 Walkthrough

Walkthrough

The DELETE handler now awaits all MFA factor deletions, returns deletion errors, and reports success only after completion. Tests cover success, failures, empty factor lists, listing errors, asynchronous completion, and method validation.

Changes

MFA factor deletion

Layer / File(s) Summary
Awaited deletion and API validation
apps/studio/pages/api/platform/auth/[ref]/users/[id]/factors.ts, apps/studio/tests/pages/api/platform/auth/[ref]/users/[id]/factors.test.ts
The handler awaits all deletion requests with Promise.all, returns the first deletion error with status 400, and includes tests for success, failures, listing errors, empty factors, asynchronous completion, and HTTP methods.

Estimated code review effort: 3 (Moderate) | ~15–30 minutes

Suggested reviewers: alaister

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary fix: awaiting MFA factor deletions before sending the response.
Description check ✅ Passed The description includes all template sections and clearly documents the bug, behavior change, tests, and validation results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

1 participant