Skip to content

fix(auth): unenroll the unverified factor after a failed registration - #2641

Merged
mandarini merged 2 commits into
supabase:masterfrom
Kjubikstronk:webauthn-cleanup-unverified-factor
Sep 2, 2026
Merged

mandarini merged 2 commits into
supabase:masterfrom
Kjubikstronk:webauthn-cleanup-unverified-factor

Conversation

@Kjubikstronk

Copy link
Copy Markdown

Closes #2640.

When _enroll fails, register() tries to clean up after itself by finding a webauthn factor with the same friendly name and unenrolling it. The lookup selects on v.status !== 'unverified', which matches a verified factor, so a failed re-registration can delete a passkey the user already had working.

The factor worth cleaning up is the unverified one the failed attempt just left behind, so the condition is inverted. That is the whole change:

-                v.status !== 'unverified'
+                v.status === 'unverified'

Two tests in passkey.methods.test.ts, which mocks fetch throughout and needs no server. One drives a failed enrollment with an existing verified factor of the same name and asserts unenroll is never called; the other does the same with an unverified factor and asserts it is unenrolled by id. Both fail on master and pass with the change, and the other 29 tests in that file are unaffected.

prettier --check is clean on both files.

@Kjubikstronk
Kjubikstronk requested review from a team as code owners August 31, 2026 08:34
@coderabbitai

coderabbitai Bot commented Aug 31, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: baaec986-346b-4abc-9792-6e7e2f8b16bd

📥 Commits

Reviewing files that changed from the base of the PR and between b3b939a and d48d45d.

📒 Files selected for processing (2)
  • packages/core/auth-js/src/lib/webauthn.ts
  • packages/core/auth-js/test/passkey.methods.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Corrected WebAuthn enrollment cleanup after a failed registration.
    • Previously verified factors are now preserved, while incomplete unverified factors are removed appropriately.

Walkthrough

The WebAuthn enrollment failure cleanup now matches factors with status === 'unverified'. It no longer unenrolls verified factors that share the requested friendly name. Regression tests cover both preservation of an existing verified factor and removal of a matching unverified factor.

Assessment against linked issues

Objective Addressed Explanation
Preserve verified passkeys after failed registration and remove only stale unverified factors [#2640] ✅

Merge Risk: 🔵 Low · up to d48d4

This change prevents a failed registration from deleting an existing verified passkey, but cleanup can still remove an unrelated unverified passkey with the same name during retries or concurrent registrations. The PR is mergeable with explicit owner awareness and follow-up to bind cleanup to the failed registration.


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.

@pkg-pr-new

pkg-pr-new Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@supabase/auth-js

npm i https://pkg.pr.new/@supabase/auth-js@2641

@supabase/functions-js

npm i https://pkg.pr.new/@supabase/functions-js@2641

@supabase/postgrest-js

npm i https://pkg.pr.new/@supabase/postgrest-js@2641

@supabase/realtime-js

npm i https://pkg.pr.new/@supabase/realtime-js@2641

@supabase/storage-js

npm i https://pkg.pr.new/@supabase/storage-js@2641

@supabase/supabase-js

npm i https://pkg.pr.new/@supabase/supabase-js@2641

commit: 97a16eb

@mandarini mandarini 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.

Hi @Kjubikstronk, thank you for contributing to Supabase! 💚

Fix is correct. FactorVerificationStatuses is a closed union of verified and unverified, so !== 'unverified' was exactly === 'verified'. Good catch, and thanks for keeping the diff to just that.

Two things before merge:

  1. Add a comment above the changed line. A one character flip is easy to revert later by mistake. State the invariant: cleanup targets a stale unverified factor so a retry can succeed, and must never select a verified one.

  2. Two more tests. Your existing two are solid. Please also cover a verified and an unverified factor sharing a name (verified survives), and no matching factor at all (unenroll never called).

Yours is the one we are taking of the PRs for this issue, the tests belong in passkey.methods.test.ts where you put them. Thanks again!

@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 81.966% (+0.3%) from 81.697% — Kjubikstronk:webauthn-cleanup-unverified-factor into supabase:master

…anup

Adds the invariant as a comment above the status check, so the one
character is not flipped back by mistake, and two cases the existing
tests did not reach: a verified and an unverified factor sharing a
friendly name, and no matching factor at all.
@Kjubikstronk

Copy link
Copy Markdown
Author

Both done in 97a16eb.

The comment. Above the status check, stating it in terms of what each status means rather than what the operator is:

// Only an unverified factor is leftover from the enrollment that just
// failed, and removing it is what lets a retry with the same name
// succeed. A verified factor of that name is a passkey the user is
// already using, so it must never be selected here.

The two tests. Both in passkey.methods.test.ts, next to the existing pair. The helper now takes a list of factors so the shared name case can be expressed directly:

  • removes only the unverified one when both share a name, asserting unenroll is called exactly once and with the unverified id, so the test fails if the search ever picks the first match by name alone.
  • does nothing when no factor of that name exists, with unenroll never called.

I checked the new tests actually bite: reverting the character back to !== 'unverified' fails three of the four, and the fourth is the no factor case, which is a control that should pass either way.

passkey.methods.test.ts is green, 33 of 33.

@mandarini

Copy link
Copy Markdown
Contributor

@Kjubikstronk i'll fix the pnpm audit issues on master and I'll ask you to rebase

@mandarini

Copy link
Copy Markdown
Contributor

@Kjubikstronk can you please rebase with master?

@mandarini
mandarini merged commit bef68e0 into supabase:master Sep 2, 2026
21 of 23 checks passed
mandarini pushed a commit to supabase/ssr that referenced this pull request Sep 2, 2026
This PR updates `@supabase/supabase-js` to v2.114.0.

**Source**: supabase-js-stable-release

---

## Release Notes

## v2.114.0

## 2.114.0 (2026-09-02)

### 🚀 Features

- **storage:** object versioning updates
([#2644](supabase/supabase-js#2644))

### 🩹 Fixes

- **auth:** unenroll the unverified factor after a failed registration
([#2641](supabase/supabase-js#2641))
- **deps:** override browserslist to patch high-severity advisories
([#2652](supabase/supabase-js#2652))

### ❤️ Thank You

- Katerina Skroumpelou @mandarini
- Miodrag Obradovic @Kjubikstronk
- Tyler Hillery
## v2.113.0

## 2.113.0 (2026-09-02)

### 🚀 Features

- **realtime:** allow wait for pg changes
([#2630](supabase/supabase-js#2630))

### ❤️ Thank You

- Filipe Cabaço @filipecabaco

This PR was created automatically.

Co-authored-by: supabase-workflow-trigger[bot] <266661614+supabase-workflow-trigger[bot]@users.noreply.github.com>
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.

auth-js: mfa.webauthn.register() deletes an existing verified passkey when a re-registration fails

3 participants