Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/two-factor-verify-echoes-live-user-row.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@objectstack/plugin-auth": patch
"@objectstack/client": patch
---

`POST /two-factor/verify-totp` and `/two-factor/verify-otp` now echo the user row as it stands when the response is written, instead of the pre-rotation snapshot the vendor closes over.

On the enrolment lane — a signed-in caller confirming a new factor — better-auth writes `twoFactorEnabled: true`, rotates the session, and only then calls the `valid(ctx)` closure it built at entry. That closure still holds the pre-rotation session, so a successful verification answered `user.twoFactorEnabled: false` to the very caller who had just switched 2FA on. An account portal reading that body renders the factor as still OFF right after enrolment, and a bearer client that caches the echoed user carries the wrong flag until its next `get-session`.

`two-factor-rotated-token-echo` already repaired the body's other stale member, `token`, on exactly these routes and on exactly this predicate — the response staged a session cookie whose token differs from the one echoed. The `user` member is stale for the same reason, so it is repaired under the same predicate rather than a new one.

- **Two narrowings, both load-bearing.** Only the members the vendor already echoed are written, so the published payload shape (`AuthWireUser`) cannot widen — better-auth's own output filter is a deny-list, and forwarding a raw row would put every column it happens to carry on the wire. And the row is re-read through `internalAdapter` by the id the response itself published, so the repair travels the same output transform that produced the echo (a driver that stores booleans as `1`/`0` cannot change a member's wire type) and can never substitute a different principal into a response.
- **`/two-factor/verify-backup-code` is untouched.** It does not rotate and already echoed the live row; it is in neither path list, its row is not read, and it is pinned as a negative control on both the in-memory engine and a real `SqlDriver` — an unconditional re-read would have "fixed" the broken lane and quietly rewritten one that was already right.
- **The failure posture is inherited.** A row read that throws or answers nothing degrades to the vendor's own echo, never to a failed verification and never to a lost `token` repair, which is written first for that reason.

`@objectstack/client` drops the `AuthTwoFactorVerificationResult.user` warning that told callers to re-read the session for the live flag; the wire shape it declares is unchanged.
17 changes: 8 additions & 9 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1068,11 +1068,7 @@ export interface AuthWireUser {
createdAt: string;
/** ISO-8601. */
updatedAt: string;
/**
* `twoFactor` plugin only. ⚠️ On the enrolment lane of `verifyTotp` this
* is echoed from the pre-flip snapshot — see
* {@link AuthTwoFactorVerificationResult}.
*/
/** `twoFactor` plugin only. */
twoFactorEnabled?: boolean;
/**
* `admin` plugin only. An open string: the vocabulary is the deployment's
Expand Down Expand Up @@ -1160,10 +1156,13 @@ export interface AuthTwoFactorVerificationResult {
*/
token: string;
/**
* ⚠️ On the ENROLMENT lane of `verifyTotp` the vendor echoes the user from
* its pre-rotation snapshot, so `twoFactorEnabled` reads `false` here
* although the flag has just flipped server-side (measured on a real SQL
* driver). Re-read the session for the live value.
* The caller, as the row stands when the response is written. The vendor
* echoes the user from its PRE-rotation snapshot on the enrolment lane, so
* `twoFactorEnabled` used to read `false` here although the flag had just
* flipped server-side; plugin-auth's `two-factor-rotated-token-echo`
* repairs that member from the row on the same rotating routes it repairs
* `token` on, so no second read is needed. The payload's shape is
* unchanged — the repair corrects values only.
*/
user: AuthWireUser;
}
Expand Down
Loading
Loading