[GH-ISSUE #3547] Server side impersonateUser() doesn't work #9639

Closed
opened 2026-04-13 05:13:47 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @j1mbo64 on GitHub (Jul 22, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3547

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

export async function testImpersonate(id: string) {

  const impersonateResult = await auth.api.impersonateUser({
    body: { userId: id },
    headers: await headers(),
  });

  console.log("Impersonation result :", impersonateResult);
  console.log("During Impersonation Session :", await getSession());

  // Stop impersonating the owner
  auth.api.stopImpersonating({
    headers: await headers(),
  });
}

Current vs. Expected behavior

Here is the impersonateResult value (works as expected)

Impersonation result : {
  session: {
    expiresAt: 2025-07-22T14:55:09.483Z,
    token: '4mCfkEIPz3k82HxgeO0pawzrq8a8Rhqa',
    createdAt: 2025-07-22T14:54:09.484Z,
    updatedAt: 2025-07-22T14:54:09.484Z,
    ipAddress: '',
    userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
    userId: 'cA06KJqI0Xrbi6AW2TvqxFgtg4PdHHmV',
    impersonatedBy: '9RuxBUf5cvN3JvNKNQ0qpOITdsLuxNxp',
    id: 'h0N1t4Xlv18nCgYKynPDZNjDa5SPbDZy'
  },
  user: {
    name: 'toto',
    email: 'toto@gmail.com',
    emailVerified: false,
    image: null,
    createdAt: 2025-07-21T14:12:46.489Z,
    updatedAt: 2025-07-21T14:12:46.489Z,
    role: 'user',
    banned: null,
    banReason: null,
    banExpires: null,
    onboarding: true,
    selectedEntityId: null,
    id: 'cA06KJqI0Xrbi6AW2TvqxFgtg4PdHHmV'
  }
}

Here is the getSession() result value

During Impersonation Session : {
  session: {
    expiresAt: 2025-07-29T13:26:33.986Z,
    token: 'rakYHAxt14TanjfgS43blXN01NvOEkU0',
    createdAt: 2025-07-22T13:26:33.987Z,
    updatedAt: 2025-07-22T13:26:33.987Z,
    ipAddress: '',
    userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
    userId: '9RuxBUf5cvN3JvNKNQ0qpOITdsLuxNxp',
    impersonatedBy: null,
    id: 'QaBuqjB96tXYlyNoDA4xFvjSvwWJ3hBU'
  },
  user: {
    name: 'admin',
    email: 'admin@gmail.com',
    emailVerified: false,
    image: null,
    createdAt: 2025-07-21T14:12:46.650Z,
    updatedAt: 2025-07-21T14:12:46.650Z,
    role: 'admin',
    banned: null,
    banReason: null,
    banExpires: null,
    onboarding: true,
    selectedEntityId: null,
    id: '9RuxBUf5cvN3JvNKNQ0qpOITdsLuxNxp'
  }
}

When getSession() I still admin user.

getSession() is a simple wrapper like this :

export async function getSession(): Promise<any | undefined> {
  const session = await auth.api.getSession({
    headers: await headers(),
  });
  return session;
}

What version of Better Auth are you using?

1.3.2

Provide environment information

- OS: ArchLinux
- Browser: Chrome

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

export const auth = betterAuth({
  user: {
    additionalFields: {
      onboarding: {
        type: "boolean",
        required: true,
        defaultValue: false,
      },
      selectedEntityId: {
        type: "string",
        required: false,
        input: true,
      },
    },
  },
  emailAndPassword: {
    enabled: true,
    // async sendResetPassword(data, request) {
    // 	// Send an email to the user with a link to reset their password
    // },
  },
  session: {
    cookieCache: {
      enabled: true,
      maxAge: 5 * 60, // Cache duration in seconds
    },
  },
  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),
  plugins: [
    admin({
      defaultRole: "user", // Set default role for new users
      impersonationSessionDuration: 60, // 1 minute
    }),
    apiKey(),
  ],
});

Additional context

No response

Originally created by @j1mbo64 on GitHub (Jul 22, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3547 ### Is this suited for github? - [ ] Yes, this is suited for github ### To Reproduce ```ts export async function testImpersonate(id: string) { const impersonateResult = await auth.api.impersonateUser({ body: { userId: id }, headers: await headers(), }); console.log("Impersonation result :", impersonateResult); console.log("During Impersonation Session :", await getSession()); // Stop impersonating the owner auth.api.stopImpersonating({ headers: await headers(), }); } ``` ### Current vs. Expected behavior Here is the `impersonateResult` value (works as expected) ``` Impersonation result : { session: { expiresAt: 2025-07-22T14:55:09.483Z, token: '4mCfkEIPz3k82HxgeO0pawzrq8a8Rhqa', createdAt: 2025-07-22T14:54:09.484Z, updatedAt: 2025-07-22T14:54:09.484Z, ipAddress: '', userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', userId: 'cA06KJqI0Xrbi6AW2TvqxFgtg4PdHHmV', impersonatedBy: '9RuxBUf5cvN3JvNKNQ0qpOITdsLuxNxp', id: 'h0N1t4Xlv18nCgYKynPDZNjDa5SPbDZy' }, user: { name: 'toto', email: 'toto@gmail.com', emailVerified: false, image: null, createdAt: 2025-07-21T14:12:46.489Z, updatedAt: 2025-07-21T14:12:46.489Z, role: 'user', banned: null, banReason: null, banExpires: null, onboarding: true, selectedEntityId: null, id: 'cA06KJqI0Xrbi6AW2TvqxFgtg4PdHHmV' } } ``` Here is the `getSession()` result value ``` During Impersonation Session : { session: { expiresAt: 2025-07-29T13:26:33.986Z, token: 'rakYHAxt14TanjfgS43blXN01NvOEkU0', createdAt: 2025-07-22T13:26:33.987Z, updatedAt: 2025-07-22T13:26:33.987Z, ipAddress: '', userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', userId: '9RuxBUf5cvN3JvNKNQ0qpOITdsLuxNxp', impersonatedBy: null, id: 'QaBuqjB96tXYlyNoDA4xFvjSvwWJ3hBU' }, user: { name: 'admin', email: 'admin@gmail.com', emailVerified: false, image: null, createdAt: 2025-07-21T14:12:46.650Z, updatedAt: 2025-07-21T14:12:46.650Z, role: 'admin', banned: null, banReason: null, banExpires: null, onboarding: true, selectedEntityId: null, id: '9RuxBUf5cvN3JvNKNQ0qpOITdsLuxNxp' } } ``` When `getSession()` I still `admin` user. `getSession()` is a simple wrapper like this : ```ts export async function getSession(): Promise<any | undefined> { const session = await auth.api.getSession({ headers: await headers(), }); return session; } ``` ### What version of Better Auth are you using? 1.3.2 ### Provide environment information ```bash - OS: ArchLinux - Browser: Chrome ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript export const auth = betterAuth({ user: { additionalFields: { onboarding: { type: "boolean", required: true, defaultValue: false, }, selectedEntityId: { type: "string", required: false, input: true, }, }, }, emailAndPassword: { enabled: true, // async sendResetPassword(data, request) { // // Send an email to the user with a link to reset their password // }, }, session: { cookieCache: { enabled: true, maxAge: 5 * 60, // Cache duration in seconds }, }, database: prismaAdapter(prisma, { provider: "postgresql", }), plugins: [ admin({ defaultRole: "user", // Set default role for new users impersonationSessionDuration: 60, // 1 minute }), apiKey(), ], }); ``` ### Additional context _No response_
GiteaMirror added the lockedbug labels 2026-04-13 05:13:47 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Jul 22, 2025):

Hi @j1mbo64! I'm Dosu and I’m helping the better-auth team.

This is expected behavior: after calling impersonateUser(), the new session token for the impersonated user is returned in the response, but your server-side context (headers/cookies) is not automatically updated. Subsequent calls like getSession() will still use the original admin session unless you explicitly pass the new session token from impersonateResult.session.token in your headers or cookies for further API calls.

To impersonate on the server side, you need to manually propagate the new session token. For example:

export async function testImpersonate(id: string) {
  const impersonateResult = await auth.api.impersonateUser({
    body: { userId: id },
    headers: await headers(),
  });

  // Use the new session token for subsequent calls
  const impersonationHeaders = {
    ...await headers(),
    'Authorization': `Bearer ${impersonateResult.session.token}`,
  };

  console.log("Impersonation result :", impersonateResult);
  console.log(
    "During Impersonation Session :",
    await auth.api.getSession({ headers: impersonationHeaders })
  );

  // Stop impersonating (pass the impersonation token)
  await auth.api.stopImpersonating({
    headers: impersonationHeaders,
  });
}

This stateless pattern is by design: Better Auth does not mutate your server-side session context automatically—you must use the new token for impersonated requests [source].

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

<!-- gh-comment-id:3103268555 --> @dosubot[bot] commented on GitHub (Jul 22, 2025): <!-- Greeting --> Hi @j1mbo64! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is expected behavior: after calling impersonateUser(), the new session token for the impersonated user is returned in the response, but your server-side context (headers/cookies) is not automatically updated. Subsequent calls like getSession() will still use the original admin session unless you explicitly pass the new session token from impersonateResult.session.token in your headers or cookies for further API calls. To impersonate on the server side, you need to manually propagate the new session token. For example: ```ts export async function testImpersonate(id: string) { const impersonateResult = await auth.api.impersonateUser({ body: { userId: id }, headers: await headers(), }); // Use the new session token for subsequent calls const impersonationHeaders = { ...await headers(), 'Authorization': `Bearer ${impersonateResult.session.token}`, }; console.log("Impersonation result :", impersonateResult); console.log( "During Impersonation Session :", await auth.api.getSession({ headers: impersonationHeaders }) ); // Stop impersonating (pass the impersonation token) await auth.api.stopImpersonating({ headers: impersonationHeaders, }); } ``` This stateless pattern is by design: Better Auth does not mutate your server-side session context automatically—you must use the new token for impersonated requests [[source]](https://github.com/better-auth/better-auth/issues/3297). <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/e7083d86-f837-49b6-bc77-6e5c5a8c8659?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/e7083d86-f837-49b6-bc77-6e5c5a8c8659?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/e7083d86-f837-49b6-bc77-6e5c5a8c8659?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/e7083d86-f837-49b6-bc77-6e5c5a8c8659?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/e7083d86-f837-49b6-bc77-6e5c5a8c8659?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/e7083d86-f837-49b6-bc77-6e5c5a8c8659?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/e7083d86-f837-49b6-bc77-6e5c5a8c8659?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3547)
Author
Owner

@DkR-Luca-Lucidera commented on GitHub (Aug 28, 2025):

i have the same error, here the code:

const {
  headers,
  response: impersonateResult
} = await auth.api.impersonateUser({
  body: {
    userId: memberInfo.user.id
  },
  headers: ctx.headers,
  returnHeaders: true,
});

// Use the new session token for subsequent calls
const impersonationHeaders = {...headers,
  Authorization: `Bearer $ {
    impersonateResult.session.token
  }`,
};

console.log("Impersonation result :", impersonateResult);
console.log("During Impersonation Session :", await auth.api.getSession({
  headers: impersonationHeaders
}), );

await auth.api.stopImpersonating({
  headers: impersonationHeaders,
});

even with ctx.headers give the same output

console output:

Impersonation result : {
  session: {
    expiresAt: 2025-08-29T08:07:18.708Z,
    token: 'fRq0PFulsOkFvLvYA6on6tz6ZaAjUqFm',
    createdAt: 2025-08-28T08:07:18.709Z,
    updatedAt: 2025-08-28T08:07:18.709Z,
    ipAddress: '',
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36',
    userId: 'zP3BtfkHLfYtjHnIrMUePqaWMXh2tbQC',
    impersonatedBy: 'Kzli40rM8C9TQ8mNz2iBzg6gYx8cqmjS',
    activeOrganizationId: null,
    id: 'jBoM3zOH41UaMSJbdGfVHlVWoxUinJCE'
  },
  user: {
    name: 'ref',
    email: 'ref@one.com',
    emailVerified: false,
    image: null,
    createdAt: 2025-08-28T07:53:48.220Z,
    updatedAt: 2025-08-28T07:53:48.220Z,
    role: 'user',
    banned: null,
    banReason: null,
    banExpires: null,
    lastName: 'one',
    id: 'zP3BtfkHLfYtjHnIrMUePqaWMXh2tbQC'
  }
}
During Impersonation Session : null
[Error [APIError]: ] {
  status: 'UNAUTHORIZED',
  body: undefined,
  headers: {},
  statusCode: 401
}
<!-- gh-comment-id:3232416037 --> @DkR-Luca-Lucidera commented on GitHub (Aug 28, 2025): i have the same error, here the code: ```ts const { headers, response: impersonateResult } = await auth.api.impersonateUser({ body: { userId: memberInfo.user.id }, headers: ctx.headers, returnHeaders: true, }); // Use the new session token for subsequent calls const impersonationHeaders = {...headers, Authorization: `Bearer $ { impersonateResult.session.token }`, }; console.log("Impersonation result :", impersonateResult); console.log("During Impersonation Session :", await auth.api.getSession({ headers: impersonationHeaders }), ); await auth.api.stopImpersonating({ headers: impersonationHeaders, }); ``` even with ctx.headers give the same output console output: ```sh Impersonation result : { session: { expiresAt: 2025-08-29T08:07:18.708Z, token: 'fRq0PFulsOkFvLvYA6on6tz6ZaAjUqFm', createdAt: 2025-08-28T08:07:18.709Z, updatedAt: 2025-08-28T08:07:18.709Z, ipAddress: '', userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36', userId: 'zP3BtfkHLfYtjHnIrMUePqaWMXh2tbQC', impersonatedBy: 'Kzli40rM8C9TQ8mNz2iBzg6gYx8cqmjS', activeOrganizationId: null, id: 'jBoM3zOH41UaMSJbdGfVHlVWoxUinJCE' }, user: { name: 'ref', email: 'ref@one.com', emailVerified: false, image: null, createdAt: 2025-08-28T07:53:48.220Z, updatedAt: 2025-08-28T07:53:48.220Z, role: 'user', banned: null, banReason: null, banExpires: null, lastName: 'one', id: 'zP3BtfkHLfYtjHnIrMUePqaWMXh2tbQC' } } During Impersonation Session : null [Error [APIError]: ] { status: 'UNAUTHORIZED', body: undefined, headers: {}, statusCode: 401 } ```
Author
Owner

@ping-maxwell commented on GitHub (Oct 8, 2025):

The reason you're running into this issue is because when calling impersonateUser, it will issue a new session and will create a set-cookie header in the response, since you didn't change the current header value to use the new session cookie, the getSession call is running off an older session, not the new one.

<!-- gh-comment-id:3381645922 --> @ping-maxwell commented on GitHub (Oct 8, 2025): The reason you're running into this issue is because when calling `impersonateUser`, it will issue a new session and will create a `set-cookie` header in the response, since you didn't change the current header value to use the new session cookie, the `getSession` call is running off an older session, not the new one.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9639