hasPermission from organization plugin doesn't work with API key authentication #2393

Closed
opened 2026-03-13 09:49:25 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @simonc on GitHub (Nov 25, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Configure BetterAuth with both the organization and apiKey plugins
  2. Set enableSessionForAPIKeys: true for the apiKey plugin
  3. Create an API key for a user who is a member of an organization
  4. Make a request with the API key in x-api-key header
  5. Call auth.api.getSession({ headers }) to get a mock session for the user
  6. Call auth.api.hasPermission({ headers, body: { permissions: { organization: ["read"] } } }), this fails with an unauthorized error

Current vs. Expected behavior

Current behavior:

  • enableSessionForAPIKeys creates a mock session that allows getSession to work
  • hasPermission fails because the mock session doesn't have activeOrganizationId
  • Calling setActiveOrganization fails with the mock session, as it has no token

Expected behavior:

  • When using enableSessionForAPIKeys, the organization plugin's hasPermission should work
  • Maybe hasPermission could accept an optional organizationId parameter to explicitly specify the organization context

What version of Better Auth are you using?

1.4.0

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 25.1.0: Mon Oct 20 19:32:41 PDT 2025; root:xnu-12377.41.6~2/RELEASE_ARM64_T6000",
    "release": "25.1.0",
    "cpuCount": 10,
    "cpuModel": "Apple M1 Max",
    "totalMemory": "64.00 GB",
    "freeMemory": "1.09 GB"
  },
  "node": {
    "version": "v24.6.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.5.1"
  },
  "frameworks": null,
  "databases": null,
  "betterAuth": {
    "version": "Unknown",
    "config": null
  }
}

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

Backend

Auth config (if applicable)

betterAuth({
  // ... other config
  plugins: [
    magicLink({ ... }),
    organization({
      ac,
      membershipLimit: 100000, // Remove when https://github.com/better-auth/better-auth/issues/5517 is merged
      organizationCreation: { ... },
      roles: { owner, admin, member },
      schema: {
        organization: { ... },
      },
    }),
    apiKey({
      enableMetadata: true,
      enableSessionForAPIKeys: true,
      defaultPrefix: "lia_sk_",
      rateLimit: {
        enabled: true,
        maxRequests: 120, // 2 requests per second (120 per 60s)
        timeWindow: 1000 * 60, // 60 seconds
      },
      requireName: true,
    }),
  ],
});

Additional context

No response

Originally created by @simonc on GitHub (Nov 25, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Configure BetterAuth with both the `organization` and `apiKey` plugins 2. Set `enableSessionForAPIKeys: true` for the apiKey plugin 3. Create an API key for a user who is a member of an organization 4. Make a request with the API key in `x-api-key` header 5. Call `auth.api.getSession({ headers })` to get a mock session for the user 6. Call `auth.api.hasPermission({ headers, body: { permissions: { organization: ["read"] } } })`, this fails with an unauthorized error ### Current vs. Expected behavior Current behavior: - `enableSessionForAPIKeys` creates a mock session that allows `getSession` to work - `hasPermission` fails because the mock session doesn't have `activeOrganizationId` - Calling `setActiveOrganization` fails with the mock session, as it has no token Expected behavior: - When using `enableSessionForAPIKeys`, the organization plugin's `hasPermission` should work - Maybe `hasPermission` could accept an optional `organizationId` parameter to explicitly specify the organization context ### What version of Better Auth are you using? 1.4.0 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.1.0: Mon Oct 20 19:32:41 PDT 2025; root:xnu-12377.41.6~2/RELEASE_ARM64_T6000", "release": "25.1.0", "cpuCount": 10, "cpuModel": "Apple M1 Max", "totalMemory": "64.00 GB", "freeMemory": "1.09 GB" }, "node": { "version": "v24.6.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.5.1" }, "frameworks": null, "databases": null, "betterAuth": { "version": "Unknown", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript betterAuth({ // ... other config plugins: [ magicLink({ ... }), organization({ ac, membershipLimit: 100000, // Remove when https://github.com/better-auth/better-auth/issues/5517 is merged organizationCreation: { ... }, roles: { owner, admin, member }, schema: { organization: { ... }, }, }), apiKey({ enableMetadata: true, enableSessionForAPIKeys: true, defaultPrefix: "lia_sk_", rateLimit: { enabled: true, maxRequests: 120, // 2 requests per second (120 per 60s) timeWindow: 1000 * 60, // 60 seconds }, requireName: true, }), ], }); ``` ### Additional context _No response_
GiteaMirror added the plugin label 2026-03-13 09:49:25 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 25, 2025):

It appears that when you use API keys with sessions enabled the generated “mock” session does not include the active organization ID, so the organization plugin’s permission check fails. The current behavior shows that getSession returns a valid session, but hasPermission can’t verify the organization context because activeOrganizationId is missing, and attempts to set an active organization with a mock session are ineffective.

A potential resolution is to either provide a way to explicitly supply an organizationId to the hasPermission call or programmatically set the active organization after obtaining the session. This could involve extending the API (or using a custom solution) so that the organization plugin can work properly even with API key mock sessions.

tag @better-auth-agent if you would like an answer from the Agent. Do you have any thoughts on a preferred workaround?

DiagramDiscordGitHub

Diagram Join Star

@better-auth-agent[bot] commented on GitHub (Nov 25, 2025): It appears that when you use API keys with sessions enabled the generated “mock” session does not include the active organization ID, so the organization plugin’s permission check fails. The current behavior shows that getSession returns a valid session, but hasPermission can’t verify the organization context because activeOrganizationId is missing, and attempts to set an active organization with a mock session are ineffective. A potential resolution is to either provide a way to explicitly supply an organizationId to the hasPermission call or programmatically set the active organization after obtaining the session. This could involve extending the API (or using a custom solution) so that the organization plugin can work properly even with API key mock sessions. tag @better-auth-agent if you would like an answer from the Agent. Do you have any thoughts on a preferred workaround? <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@dosubot[bot] commented on GitHub (Nov 25, 2025):

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

This is expected behavior in BetterAuth v1.4.0. When using API key authentication with enableSessionForAPIKeys, the mock session does not persist organization context like activeOrganizationId, so permission checks that rely on it (such as hasPermission) will fail unless you provide the organization context explicitly. The recommended solution is to always pass organizationId in the request body when calling hasPermission with an API key session. For example:

auth.api.hasPermission({
  headers,
  body: {
    permissions: { organization: ["read"] },
    organizationId: "your-org-id"
  }
})

This pattern is supported by the organization plugin: if organizationId is provided, it will be used for the permission check; otherwise, it falls back to activeOrganizationId from the session, which is missing in API key mock sessions. If neither is present, you'll get a NO_ACTIVE_ORGANIZATION error see code.

There is no built-in way to persist activeOrganizationId in API key sessions, and calling setActiveOrganization will fail because mock sessions do not have a valid session token see code. This is a structural limitation, not a bug see discussion.

Best practice: always include organizationId in permission checks when using API keys. This makes your API stateless and avoids session context issues.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

@dosubot[bot] commented on GitHub (Nov 25, 2025): <!-- Greeting --> Hi @simonc! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> This is expected behavior in BetterAuth v1.4.0. When using API key authentication with `enableSessionForAPIKeys`, the mock session does not persist organization context like `activeOrganizationId`, so permission checks that rely on it (such as `hasPermission`) will fail unless you provide the organization context explicitly. The recommended solution is to always pass `organizationId` in the request body when calling `hasPermission` with an API key session. For example: ```typescript auth.api.hasPermission({ headers, body: { permissions: { organization: ["read"] }, organizationId: "your-org-id" } }) ``` This pattern is supported by the organization plugin: if `organizationId` is provided, it will be used for the permission check; otherwise, it falls back to `activeOrganizationId` from the session, which is missing in API key mock sessions. If neither is present, you'll get a `NO_ACTIVE_ORGANIZATION` error [see code](https://github.com/better-auth/better-auth/blob/cb5c2fc2604090247f8bb9a33eae03a5fdc1ab14/packages/better-auth/src/plugins/organization/organization.ts#L151-L1211). There is no built-in way to persist `activeOrganizationId` in API key sessions, and calling `setActiveOrganization` will fail because mock sessions do not have a valid session token [see code](https://github.com/better-auth/better-auth/blob/cb5c2fc2604090247f8bb9a33eae03a5fdc1ab14/packages/better-auth/src/plugins/api-key/index.ts#L138-L226). This is a structural limitation, not a bug [see discussion](https://github.com/better-auth/better-auth/issues/5500). Best practice: always include `organizationId` in permission checks when using API keys. This makes your API stateless and avoids session context issues. <!-- 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/2fb9c8a8-ac15-49b8-b3ff-8c9f181790cc?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/2fb9c8a8-ac15-49b8-b3ff-8c9f181790cc?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/2fb9c8a8-ac15-49b8-b3ff-8c9f181790cc?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/2fb9c8a8-ac15-49b8-b3ff-8c9f181790cc?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/2fb9c8a8-ac15-49b8-b3ff-8c9f181790cc?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/2fb9c8a8-ac15-49b8-b3ff-8c9f181790cc?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/2fb9c8a8-ac15-49b8-b3ff-8c9f181790cc?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20is%20expected%20behavior%20in%20BetterAuth%20v1.4.0.%20When%20using%20API%20key%20authentication%20with%20%60enableSessionForAPIKeys%60%2C%20the%20mock%20session%20does%20not%20persist%20organization%20context%20like%20%60activeOrganizationId%60%2C%20so%20permission%20checks%20that%20rely%20on%20it%20%28such%20as%20%60hasPermission%60%29%20will%20fail%20unless%20you%20provide%20the%20organization%20context%20explicitly.%20The%20recommended%20solution%20is%20to%20always%20pass%20%60organizationId%60%20in%20the%20request%20body%20when%20calling%20%60hasPermission%60%20with%20an%20API%20key%20session.%20For%20example%3A%0A%0A%60%60%60typescript%0Aauth.api.hasPermission%28%7B%0A%20%20headers%2C%0A%20%20body%3A%20%7B%0A%20%20%20%20permissions%3A%20%7B%20organization%3A%20%5B%22read%22%5D%20%7D%2C%0A%20%20%20%20organizationId%3A%20%22your-org-id%22%0A%20%20%7D%0A%7D%29%0A%60%60%60%0A%0AThis%20pattern%20is%20supported%20by%20the%20organization%20plugin%3A%20if%20%60organizationId%60%20is%20provided%2C%20it%20will%20be%20used%20for%20the%20permission%20check%3B%20otherwise%2C%20it%20falls%20back%20to%20%60activeOrganizationId%60%20from%20the%20session%2C%20which%20is%20missing%20in%20API%20key%20mock%20sessions.%20If%20neither%20is%20present%2C%20you%27ll%20get%20a%20%60NO_ACTIVE_ORGANIZATION%60%20error%C2%A0%5Bsee%20code%5D%28https%3A//github.com/better-auth/better-auth/blob/cb5c2fc2604090247f8bb9a33eae03a5fdc1ab14/packages/better-auth/src/plugins/organization/organization.ts%23L151-L1211%29.%0A%0AThere%20is%20no%20built-in%20way%20to%20persist%20%60activeOrganizationId%60%20in%20API%20key%20sessions%2C%20and%20calling%20%60setActiveOrganization%60%20will%20fail%20because%20mock%20sessions%20do%20not%20have%20a%20valid%20session%20token%C2%A0%5Bsee%20code%5D%28https%3A//github.com/better-auth/better-auth/blob/cb5c2fc2604090247f8bb9a33eae03a5fdc1ab14/packages/better-auth/src/plugins/api-key/index.ts%23L138-L226%29.%20This%20is%20a%20structural%20limitation%2C%20not%20a%20bug%C2%A0%5Bsee%20discussion%5D%28https%3A//github.com/better-auth/better-auth/issues/5500%29.%0A%0ABest%20practice%3A%20always%20include%20%60organizationId%60%20in%20permission%20checks%20when%20using%20API%20keys.%20This%20makes%20your%20API%20stateless%20and%20avoids%20session%20context%20issues.)&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/6307)
Author
Owner

@simonc commented on GitHub (Nov 25, 2025):

Thank you. I went through the documentation but I should have looked at the code.

@simonc commented on GitHub (Nov 25, 2025): Thank you. I went through the documentation but I should have looked at the code.
Author
Owner

@xmlking commented on GitHub (Feb 14, 2026):

I experienced same problem today when trying to get JWT token with APIKey

curl http://localhost:3000/api/auth/token \
  --header 'Accept: */*' \
  --header 'x-api-key: my_token'

Response missing orgId, orgMemberId etc

Wish it is documented in APIKey/JWT plugins docs

plugin config

  apiKey({
    enableSessionForAPIKeys: true,  
    enableMetadata: true,
    requireName: true,
  }),
  jwt({
    jwt: {
      definePayload: ({ user, session }) => {
        // Only include essential user information for API authentication
        return {
          sub: user.id,
          name: user.name,
          role: user.role,
          banned: user.banned,
          email: user.email,
          emailVerified: user.emailVerified,
          orgId: session.activeOrganizationId,
          orgRole: session.activeOrganizationMemberRole,
          orgMemberId: session.activeOrganizationMemberId,
        };
      }
    },
  }),
@xmlking commented on GitHub (Feb 14, 2026): I experienced same problem today when trying to get **JWT** token with **APIKey** ```shell curl http://localhost:3000/api/auth/token \ --header 'Accept: */*' \ --header 'x-api-key: my_token' ``` Response missing `orgId`, `orgMemberId` etc > Wish it is documented in APIKey/JWT plugins docs ### plugin config ```ts apiKey({ enableSessionForAPIKeys: true, enableMetadata: true, requireName: true, }), jwt({ jwt: { definePayload: ({ user, session }) => { // Only include essential user information for API authentication return { sub: user.id, name: user.name, role: user.role, banned: user.banned, email: user.email, emailVerified: user.emailVerified, orgId: session.activeOrganizationId, orgRole: session.activeOrganizationMemberRole, orgMemberId: session.activeOrganizationMemberId, }; } }, }), ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2393