Skip to content

fix(auth): persist passkey MFA metadata under system DB context (#2210)#2211

Open
bdunncompany wants to merge 1 commit into
LanternOps:mainfrom
bdunncompany:fix/2210-passkey-mfa-rls-context
Open

fix(auth): persist passkey MFA metadata under system DB context (#2210)#2211
bdunncompany wants to merge 1 commit into
LanternOps:mainfrom
bdunncompany:fix/2210-passkey-mfa-rls-context

Conversation

@bdunncompany

Copy link
Copy Markdown
Collaborator

Fixes #2210.

Root cause

POST /auth/mfa/passkey/verify persists the authenticator's advanced signature counter + last_used_at with a bare db.update(userPasskeys). Passkey MFA runs before the user is authenticated, so there is no user RLS context. user_passkeys is a Shape-6 (user-scoped) table — its policy allows a write only when user_id = breeze_current_user_id() or scope = 'system'. Under breeze_app with neither satisfied, the update silently matches 0 rows:

  • Last used stays Never in User Profile → Passkeys.
  • The WebAuthn signature counter never advances — so clone/replay detection is defeated (the security-relevant half, not just cosmetic).

The users.last_login_at update ~30 lines below in the same handler already wraps itself in withSystemDbAccessContext for exactly this reason (#1375); the passkey update was missed.

Fix

Wrap the user_passkeys update in withSystemDbAccessContext, matching the sibling users update. One statement; no schema or behavior change beyond making the write actually land.

Test

Adds passkeyMfaVerify.integration.test.ts — drives the real handler against real Postgres (breeze_app, RLS enforced) + Redis, stubbing only the WebAuthn assertion verification (same pattern as ssoPartnerLogin.integration.test.ts). It seeds the pre-auth pending-MFA record, posts a successful verify, and asserts last_used_at and counter are persisted. Verified fail-before / pass-after: against the unfixed handler it fails with expected null not to be null (the exact bug); with the fix it passes.

Local gate (OrbStack, node 22.20.0, pnpm 10.33.4)

  • tsc --noEmit --project apps/api/tsconfig.json (exact CI cmd) — clean
  • eslint on both changed files — clean
  • New integration test — pass (proven fail-before/pass-after)
  • silent-write-contract + dbContextTripwire write-context guards — 11 pass
  • Code-only change (no deps/Dockerfile/Go touched), so Trivy / govulncheck / pnpm-audit have no delta.

…ernOps#2210)

The POST /auth/mfa/passkey/verify handler updated user_passkeys (counter,
device_type, backed_up, last_used_at) with a bare db.update(). Passkey MFA
runs before the user is authenticated, so there is no user RLS context and
the update silently matched 0 rows under breeze_app (user_passkeys Shape 6:
user_id = breeze_current_user_id() OR scope = 'system'). Result: 'Last used'
stayed Never AND the WebAuthn signature counter never advanced, defeating
clone detection.

Wrap the update in withSystemDbAccessContext, mirroring the users.last_login_at
update in the same handler (LanternOps#1375). Adds a real-DB regression test that drives
the handler against breeze_app RLS and fails without the fix.
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.

Passkey MFA verification does not update Last used or credential counter

1 participant