Skip to content

better-auth bearer plugin lets a bearer session silently shadow an impersonation the server just created — /admin/impersonate-user returns 200 and is a no-op for any bearer client #8243

Description

@yinlianghui

Filed from the objectui seat as the server-side half of objectui#4467's measurement (defence-in-depth companion to the console-side fix ruled there — option A, the data lane adopting set-auth-token rotation). Contract question — objectstack lane; this seat lands no code here.

The mechanism (measured, better-auth 1.6.26)

  1. A client authenticates with Authorization: Bearer <token>; better-auth's bearer plugin before-hook overwrites the request's session cookie with the bearer token (dist/plugins/bearer/index.mjs: setRequestCookie(headers, authCookies.sessionToken.name, decodedToken)).
  2. POST /api/v1/auth/admin/impersonate-user succeeds (200), sets the impersonation session cookie, emits the impersonated token via set-auth-token, and records admin_session.
  3. Every subsequent bearer-authenticated request resolves to the admin's session — the bearer beats the cookie the impersonation just set. The framework's own data routes resolve identity through the same path (runtime/src/security/resolve-session-principal.ts:51, http-dispatcher.ts:940), so writes during "impersonation" are attributed to the ADMIN.

Net: for any bearer client (the console after every normal sign-in), impersonation returns success and does nothing — the worst shape, a 200 no-op on a security-relevant endpoint.

The defence-in-depth ask (whichever fits the auth contract)

  • Reject a bearer-authenticated impersonate-user unless the caller can adopt the rotation (or always: force clients through the rotation they must honor), OR
  • invalidate/rotate the caller's bearer as part of impersonation, so the impersonated session is the only one that resolves afterwards.

Either makes "impersonation succeeded but did not take effect" structurally impossible server-side, independent of client lane discipline. The console-side fix (objectui#4467 option A) closes the live gap for this client; this card is about the class.

Also relevant: stop-impersonating resolves the admin via the admin_session COOKIE only — in cookie-blocked deployments (the exact context bearer() exists for) the exit path cannot work; worth deciding whether the endpoint should also honor a bearer-carried recovery.

Refs: objectui#4467 (measurement + console ruling), objectui#4304 (duplicate, closed).

Filed unassigned for the objectstack triage/decision lane.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions