[GH-ISSUE #8778] auth.api.createApiKey with references: "organization" requires userId despite docs saying organizationId alone is sufficient #11188

Open
opened 2026-04-13 07:32:40 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @Peterrallojay on GitHub (Mar 25, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8778

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Configure the api-key plugin with references: "organization":
  apiKey({
    defaultPrefix: 'dp_live_',
    references: 'organization',
    rateLimit: { enabled: false },
  })
  1. Call auth.api.createApiKey server-side with only organizationId (no session, no headers, no userId):
  const result = await auth.api.createApiKey({
    body: {
      organizationId: "a5fa9714-6748-42a1-bd64-c54dadcbf012",
      name: "my-key",
      permissions: { mesa: ["admin"] },
   },
 });
  1. Observe APIError: Unauthorized or invalid session with code UNAUTHORIZED_SESSION and status 401.

Current vs. Expected behavior

Current: Server-side auth.api.createApiKey with references: "organization" throws UNAUTHORIZED_SESSION when only organizationId is passed — even though the docs state that organizationId alone is sufficient for organization-owned keys.

It appears the root cause is in @better-auth/api-key@1.5.6 dist/index.mjs lines 737-746:

if (referencesType === "organization") {
    const orgId = ctx.body.organizationId;
    if (!orgId) {
        throw APIError("BAD_REQUEST", "ORGANIZATION_ID_REQUIRED");
    }
    const userId = session?.user.id || ctx.body.userId;
    if (!userId) throw APIError("UNAUTHORIZED", "UNAUTHORIZED_SESSION");
    await checkOrgApiKeyPermission(ctx, userId, orgId, "create");
    referenceId = orgId;
}

Line 743-744 unconditionally requires a userId (from session or body) even for org-owned keys. If neither is present, it throws.

Expected: For organization-owned keys created server-side passing organizationId alone should work consistent with the docs which state:
▎ "If you're creating an API key on the server, without access to headers, you must pass the userId property (for user-owned keys) or organizationId (for organization-owned keys)."

What version of Better Auth are you using?

1.5.6

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 25.3.0: Wed Jan 28 20:54:46 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T6000",
    "release": "25.3.0",
    "cpuCount": 10,
    "cpuModel": "Apple M1 Max",
    "totalMemory": "64.00 GB",
    "freeMemory": "4.90 GB"
  },
  "node": {
    "version": "v25.7.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.10.1"
  },
  "frameworks": null,
  "databases": null,
  "betterAuth": {
    "version": "Unknown",
    "config": null
  }
}

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

Documentation, Package, Backend

Auth config (if applicable)

import { betterAuth } from "better-auth";
import { apiKey } from "@better-auth/api-key";

export const auth = betterAuth({
  plugins: [
    apiKey({
      defaultPrefix: "dp_live_",
       references: "organization",
       rateLimit: { enabled: false },
    }),
  ],
});

Additional context

No response

Originally created by @Peterrallojay on GitHub (Mar 25, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8778 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Configure the api-key plugin with `references: "organization"`: ```ts apiKey({ defaultPrefix: 'dp_live_', references: 'organization', rateLimit: { enabled: false }, }) ``` 2. Call auth.api.createApiKey server-side with only organizationId (no session, no headers, no userId): ```ts const result = await auth.api.createApiKey({ body: { organizationId: "a5fa9714-6748-42a1-bd64-c54dadcbf012", name: "my-key", permissions: { mesa: ["admin"] }, }, }); ``` 3. Observe APIError: Unauthorized or invalid session with code UNAUTHORIZED_SESSION and status 401. ### Current vs. Expected behavior Current: Server-side auth.api.createApiKey with references: "organization" throws UNAUTHORIZED_SESSION when only organizationId is passed — even though the docs state that organizationId alone is sufficient for organization-owned keys. It appears the root cause is in @better-auth/api-key@1.5.6 dist/index.mjs lines 737-746: ```ts if (referencesType === "organization") { const orgId = ctx.body.organizationId; if (!orgId) { throw APIError("BAD_REQUEST", "ORGANIZATION_ID_REQUIRED"); } const userId = session?.user.id || ctx.body.userId; if (!userId) throw APIError("UNAUTHORIZED", "UNAUTHORIZED_SESSION"); await checkOrgApiKeyPermission(ctx, userId, orgId, "create"); referenceId = orgId; } ``` Line 743-744 unconditionally requires a userId (from session or body) even for org-owned keys. If neither is present, it throws. Expected: For organization-owned keys created server-side passing organizationId alone should work consistent with the [docs](https://www.better-auth.com/docs/plugins/api-key) which state: ▎ "If you're creating an API key on the server, without access to headers, you must pass the userId property (for user-owned keys) or organizationId (for organization-owned keys)." ### What version of Better Auth are you using? 1.5.6 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.3.0: Wed Jan 28 20:54:46 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T6000", "release": "25.3.0", "cpuCount": 10, "cpuModel": "Apple M1 Max", "totalMemory": "64.00 GB", "freeMemory": "4.90 GB" }, "node": { "version": "v25.7.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.10.1" }, "frameworks": null, "databases": null, "betterAuth": { "version": "Unknown", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Documentation, Package, Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; import { apiKey } from "@better-auth/api-key"; export const auth = betterAuth({ plugins: [ apiKey({ defaultPrefix: "dp_live_", references: "organization", rateLimit: { enabled: false }, }), ], }); ``` ### Additional context _No response_
GiteaMirror added the securitybugorganization labels 2026-04-13 07:32:40 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Mar 25, 2026):

You've identified a real discrepancy between the documentation and the actual implementation.

The documentation is misleading. The docs suggest that organizationId alone is sufficient for organization-owned keys, but the actual code requires both userId and organizationId.

This is because Better Auth API keys are always user-scoped — there's no true "organization-owned" key concept. The references: "organization" setting adds an organizational reference to the key, but the key is still fundamentally owned by a specific user. The userId is required to:

  1. Verify the user is a member of the organization
  2. Check they have the apiKey: ["create"] permission
  3. Establish the execution context for operations using that key

Workaround: Provide both userId and organizationId:

const result = await auth.api.createApiKey({
  body: {
    organizationId: "a5fa9714-6748-42a1-bd64-c54dadcbf012",
    userId: "your-user-id-here", // Required even for org keys
    name: "my-key",
    permissions: { mesa: ["admin"] },
  },
});

For organization-level automation, the common pattern is to create a dedicated service/admin user within the organization and create API keys owned by that user.

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

<!-- gh-comment-id:4130546725 --> @dosubot[bot] commented on GitHub (Mar 25, 2026): <!-- Answer --> You've identified a real discrepancy between the documentation and the actual implementation. **The documentation is misleading.** The [docs](https://www.better-auth.com/docs/plugins/api-key) suggest that `organizationId` alone is sufficient for organization-owned keys, but the [actual code](https://github.com/better-auth/better-auth/blob/163d76fb4ab63c3ed0bdfa614a961cfece550010/packages/api-key/src/routes/create-api-key.ts#L332-L335) requires **both** `userId` and `organizationId`. This is because Better Auth API keys are **always user-scoped** — there's no true "organization-owned" key concept. The `references: "organization"` setting adds an organizational reference to the key, but the key is still fundamentally owned by a specific user. The `userId` is required to: 1. Verify the user is a member of the organization 2. Check they have the `apiKey: ["create"]` permission 3. Establish the execution context for operations using that key **Workaround:** Provide both `userId` and `organizationId`: ```typescript const result = await auth.api.createApiKey({ body: { organizationId: "a5fa9714-6748-42a1-bd64-c54dadcbf012", userId: "your-user-id-here", // Required even for org keys name: "my-key", permissions: { mesa: ["admin"] }, }, }); ``` For organization-level automation, the common pattern is to create a dedicated service/admin user within the organization and create API keys owned by that user. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=brand-link).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/9951ba89-13c4-4d81-8e5a-3c0415a8978e?feedback_type=great_response&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/9951ba89-13c4-4d81-8e5a-3c0415a8978e?feedback_type=irrelevant_answer&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/9951ba89-13c4-4d81-8e5a-3c0415a8978e?feedback_type=incorrect_sources&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/9951ba89-13c4-4d81-8e5a-3c0415a8978e?feedback_type=too_verbose&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/9951ba89-13c4-4d81-8e5a-3c0415a8978e?feedback_type=hallucination&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/9951ba89-13c4-4d81-8e5a-3c0415a8978e?feedback_type=bug_report&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-bug_report) | [Other](https://app.dosu.dev/response-feedback/9951ba89-13c4-4d81-8e5a-3c0415a8978e?feedback_type=other&utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=feedback-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&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=chat-badge)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=You%27ve%20identified%20a%20real%20discrepancy%20between%20the%20documentation%20and%20the%20actual%20implementation.%0A%0A%2A%2AThe%20documentation%20is%20misleading.%2A%2A%20The%20%5Bdocs%5D%28https%3A//www.better-auth.com/docs/plugins/api-key%29%20suggest%20that%20%60organizationId%60%20alone%20is%20sufficient%20for%20organization-owned%20keys%2C%20but%20the%20%5Bactual%20code%5D%28https%3A//github.com/better-auth/better-auth/blob/163d76fb4ab63c3ed0bdfa614a961cfece550010/packages/api-key/src/routes/create-api-key.ts%23L332-L335%29%20requires%20%2A%2Aboth%2A%2A%20%60userId%60%20and%20%60organizationId%60.%0A%0AThis%20is%20because%20Better%20Auth%20API%20keys%20are%20%2A%2Aalways%20user-scoped%2A%2A%20%E2%80%94%20there%27s%20no%20true%20%22organization-owned%22%20key%20concept.%20The%20%60references%3A%20%22organization%22%60%20setting%20adds%20an%20organizational%20reference%20to%20the%20key%2C%20but%20the%20key%20is%20still%20fundamentally%20owned%20by%20a%20specific%20user.%20The%20%60userId%60%20is%20required%20to%3A%0A1.%20Verify%20the%20user%20is%20a%20member%20of%20the%20organization%0A2.%20Check%20they%20have%20the%20%60apiKey%3A%20%5B%22create%22%5D%60%20permission%0A3.%20Establish%20the%20execution%20context%20for%20operations%20using%20that%20key%0A%0A%2A%2AWorkaround%3A%2A%2A%20Provide%20both%20%60userId%60%20and%20%60organizationId%60%3A%0A%0A%60%60%60typescript%0Aconst%20result%20%3D%20await%20auth.api.createApiKey%28%7B%0A%20%20body%3A%20%7B%0A%20%20%20%20organizationId%3A%20%22a5fa9714-6748-42a1-bd64-c54dadcbf012%22%2C%0A%20%20%20%20userId%3A%20%22your-user-id-here%22%2C%20//%20Required%20even%20for%20org%20keys%0A%20%20%20%20name%3A%20%22my-key%22%2C%0A%20%20%20%20permissions%3A%20%7B%20mesa%3A%20%5B%22admin%22%5D%20%7D%2C%0A%20%20%7D%2C%0A%7D%29%3B%0A%60%60%60%0A%0AFor%20organization-level%20automation%2C%20the%20common%20pattern%20is%20to%20create%20a%20dedicated%20service/admin%20user%20within%20the%20organization%20and%20create%20API%20keys%20owned%20by%20that%20user.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-assistant&utm_content=join-discord)&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/8778)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#11188