[GH-ISSUE #3645] Admin plugin - Function userHasPermission server side #18303

Closed
opened 2026-04-15 16:43:48 -05:00 by GiteaMirror · 6 comments
Owner

Originally created by @Clems02 on GitHub (Jul 27, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3645

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Current vs. Expected behavior

I'm using the server-side API to check if the user has permissions to access a section of my application.
When the user is logged in and has the correct roles, they can access the section. If they don't have the correct roles, then they can't access it. So far, everything's working.
However, if the user with the correct permissions logs out, they retain access to the section. Indeed, the userHasPermission function returns:{ error: null, success: true }.
I can't figure out why. However, the source code provides for handling when the session is null:https://github.com/better-auth/better-auth/blob/e7af3aa80c0cce284ed7ef3b61056322e50cd964/packages/better-auth/src/plugins/admin/admin.ts#L1452

			if (
				!session &&
				(ctx.request || ctx.headers) &&
				!ctx.body.userId &&
				!ctx.body.role
			) {
				throw new APIError("UNAUTHORIZED");
			}
			const user =
				session?.user ||
				((await ctx.context.internalAdapter.findUserById(
					ctx.body.userId as string,
				)) as { role?: string; id: string }) ||
				(ctx.body.role ? { id: "", role: ctx.body.role } : null);
			if (!user) {
				throw new APIError("BAD_REQUEST", {
					message: "user not found",
				});
			}`

The only solution found so far is to check the session before checking the permissions.

export const canAccesAdminSection = async () => {
const session = await getSession();

if (!session) return false;

const response = await auth.api.userHasPermission({
body: {
permissions: {
article: ["create"],
},
},
});

return response.success;
};

What version of Better Auth are you using?

1.2.8

Provide environment information

Browser Arc

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

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

No response

Originally created by @Clems02 on GitHub (Jul 27, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3645 ### Is this suited for github? - [ ] Yes, this is suited for github ### To Reproduce - ### Current vs. Expected behavior I'm using the server-side API to check if the user has permissions to access a section of my application. When the user is logged in and has the correct roles, they can access the section. If they don't have the correct roles, then they can't access it. So far, everything's working. However, if the user with the correct permissions logs out, they retain access to the section. Indeed, the userHasPermission function returns:{ error: null, success: true }. I can't figure out why. However, the source code provides for handling when the session is null:[https://github.com/better-auth/better-auth/blob/e7af3aa80c0cce284ed7ef3b61056322e50cd964/packages/better-auth/src/plugins/admin/admin.ts#L1452](https://github.com/better-auth/better-auth/url) if ( !session && (ctx.request || ctx.headers) && !ctx.body.userId && !ctx.body.role ) { throw new APIError("UNAUTHORIZED"); } const user = session?.user || ((await ctx.context.internalAdapter.findUserById( ctx.body.userId as string, )) as { role?: string; id: string }) || (ctx.body.role ? { id: "", role: ctx.body.role } : null); if (!user) { throw new APIError("BAD_REQUEST", { message: "user not found", }); }` The only solution found so far is to check the session before checking the permissions. export const canAccesAdminSection = async () => { const session = await getSession(); if (!session) return false; const response = await auth.api.userHasPermission({ body: { permissions: { article: ["create"], }, }, }); return response.success; }; ### What version of Better Auth are you using? 1.2.8 ### Provide environment information ```bash Browser Arc ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-15 16:43:48 -05:00
Author
Owner

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

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

The behavior you're seeing is due to how userHasPermission is implemented: if there's no session, but userId or role is present in the request body, the function will still attempt to check permissions for that user or role. This means that after logout, if userId or role is still being sent, permission checks can succeed even without an active session. This is not a bug, but it does mean you need to explicitly check for a valid session before calling userHasPermission to ensure secure access control—your workaround is the correct approach for now. There are no existing issues or docs addressing this directly, but the code confirms this logic is intentional in v1.2.8 (source).

To reply, just mention @dosu.


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

<!-- gh-comment-id:3124416067 --> @dosubot[bot] commented on GitHub (Jul 27, 2025): <!-- Greeting --> Hi @Clems02! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The behavior you're seeing is due to how userHasPermission is implemented: if there's no session, but userId or role is present in the request body, the function will still attempt to check permissions for that user or role. This means that after logout, if userId or role is still being sent, permission checks can succeed even without an active session. This is not a bug, but it does mean you need to explicitly check for a valid session before calling userHasPermission to ensure secure access control—your workaround is the correct approach for now. There are no existing issues or docs addressing this directly, but the code confirms this logic is intentional in v1.2.8 [(source)](https://github.com/better-auth/better-auth/blob/e7af3aa80c0cce284ed7ef3b61056322e50cd964/packages/better-auth/src/plugins/admin/admin.ts). <!-- 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/aa0a7cbf-43f9-482b-9013-133563505511?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/aa0a7cbf-43f9-482b-9013-133563505511?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/aa0a7cbf-43f9-482b-9013-133563505511?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/aa0a7cbf-43f9-482b-9013-133563505511?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/aa0a7cbf-43f9-482b-9013-133563505511?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/aa0a7cbf-43f9-482b-9013-133563505511?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/aa0a7cbf-43f9-482b-9013-133563505511?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/3645)
Author
Owner

@Clems02 commented on GitHub (Jul 27, 2025):

@dosu

In order to avoid making two requests in my permissions check function (the session one and the permissions one), how can I stop passing the userID in my request? How can I empty everything when the user logs out?

<!-- gh-comment-id:3124435241 --> @Clems02 commented on GitHub (Jul 27, 2025): @dosu In order to avoid making two requests in my permissions check function (the session one and the permissions one), how can I stop passing the userID in my request? How can I empty everything when the user logs out?
Author
Owner

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

You only need to pass userId (or role) in your permission check request if you want to check permissions for a user other than the currently authenticated one. By default, better-auth uses the session to determine the user, so if you omit userId and role from your request body, only the session will be used for permission checks.

To avoid sending userId after logout, make sure your app clears any user-related state (like userId or role in memory, localStorage, etc) when the user logs out. The better-auth signOut endpoint clears the session and session cookie, but your app is responsible for not including userId/role in future requests after logout—just reference the cleared session state in your permission check function.

A typical pattern is:

export const canAccessAdminSection = async () => {
  // Don't store userId/role in your app state after logout
  const response = await auth.api.userHasPermission({
    body: {
      permissions: {
        article: ["create"],
      },
      // Don't include userId or role here unless you want to override the session
    },
  });
  return response.success;
};

On logout, clear any userId/role from your app state (for example, set them to null or remove them from localStorage). This way, your permission checks will only use the session, and after logout, the session will be gone, so permission checks will fail as expected.
Reference: better-auth session management

To reply, just mention @dosu.


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

<!-- gh-comment-id:3124437352 --> @dosubot[bot] commented on GitHub (Jul 27, 2025): <!-- Answer --> You only need to pass userId (or role) in your permission check request if you want to check permissions for a user other than the currently authenticated one. By default, better-auth uses the session to determine the user, so if you omit userId and role from your request body, only the session will be used for permission checks. To avoid sending userId after logout, make sure your app clears any user-related state (like userId or role in memory, localStorage, etc) when the user logs out. The better-auth signOut endpoint clears the session and session cookie, but your app is responsible for not including userId/role in future requests after logout—just reference the cleared session state in your permission check function. A typical pattern is: ```ts export const canAccessAdminSection = async () => { // Don't store userId/role in your app state after logout const response = await auth.api.userHasPermission({ body: { permissions: { article: ["create"], }, // Don't include userId or role here unless you want to override the session }, }); return response.success; }; ``` On logout, clear any userId/role from your app state (for example, set them to null or remove them from localStorage). This way, your permission checks will only use the session, and after logout, the session will be gone, so permission checks will fail as expected. [Reference: better-auth session management](https://better-auth.com/docs/concepts/session-management) <!-- 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/b522795b-d36a-473d-9e67-b0475f7417b6?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/b522795b-d36a-473d-9e67-b0475f7417b6?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/b522795b-d36a-473d-9e67-b0475f7417b6?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/b522795b-d36a-473d-9e67-b0475f7417b6?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/b522795b-d36a-473d-9e67-b0475f7417b6?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/b522795b-d36a-473d-9e67-b0475f7417b6?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/b522795b-d36a-473d-9e67-b0475f7417b6?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&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/3645)
Author
Owner

@KodingDev commented on GitHub (Aug 27, 2025):

Any updates on this? Even when directly passing the headers I need to try-catch the api request which makes it pretty convoluted.

export default async function TestPage() {
  const hasPermission = await auth.api.userHasPermission({
    headers: await headers(),
    body: {
      permissions: {
        developer: ["debug"],
      },
    },
  });

  if (!hasPermission.success) forbidden();

  return <>Authenticated</>;
}
<!-- gh-comment-id:3228484214 --> @KodingDev commented on GitHub (Aug 27, 2025): Any updates on this? Even when directly passing the headers I need to try-catch the api request which makes it pretty convoluted. ```tsx export default async function TestPage() { const hasPermission = await auth.api.userHasPermission({ headers: await headers(), body: { permissions: { developer: ["debug"], }, }, }); if (!hasPermission.success) forbidden(); return <>Authenticated</>; } ```
Author
Owner

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

To allow for session checks you can pass the headers and not pass role or userId.
Something like what @KodingDev has is correct.

Regarding @KodingDev 's issue about having to deal with both try-catch as well as the .success:

auth.api throwing is normal, if you're using any auth.api you're expected to have a try-catch to catch any potential errors. The success value is specifically related to the userHasPermission endpoint regarding wether the user actually has permission or not.

<!-- gh-comment-id:3381583660 --> @ping-maxwell commented on GitHub (Oct 8, 2025): To allow for session checks you can pass the headers and not pass `role` or `userId`. Something like what @KodingDev has is correct. Regarding @KodingDev 's issue about having to deal with both try-catch as well as the .success: auth.api throwing is normal, if you're using any auth.api you're expected to have a try-catch to catch any potential errors. The `success` value is specifically related to the `userHasPermission` endpoint regarding wether the user actually has permission or not.
Author
Owner

@davbrito commented on GitHub (Nov 21, 2025):

Why doesn't it has a sync version like authClient?

<!-- gh-comment-id:3563483738 --> @davbrito commented on GitHub (Nov 21, 2025): Why doesn't it has a sync version like authClient?
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#18303