[GH-ISSUE #6710] Organization Plugin: "inviteMember" doesn't check if role actually exists. #10597

Closed
opened 2026-04-13 06:50:27 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @joshsedl on GitHub (Dec 12, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/6710

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Problem

Currently, when creating an invitation through "authClient.organization.inviteMember()", we can provide any string as the "role" parameter, even if this role doesn't exist (this problem might also exist for the "team" parameter, but we are currently not using "teams").

Steps to reproduce

  • Check your auth.ts and auth-client.ts and check that the randomString role doesn't exist.
  • Execute authClient.organization.inviteMember() with role set to randomString.
  • See that the Invitation is sent, even if the role is NOT valid.

Proposed solution

  • Add role validation based on the roles set in auth.ts.

Current vs. Expected behavior

See above.

What version of Better Auth are you using?

1.4.5

System info

{
  "system": {
    "platform": "linux",
    "arch": "x64",
    "version": "#1 SMP PREEMPT_DYNAMIC Thu Jun  5 18:30:46 UTC 2025",
    "release": "6.6.87.2-microsoft-standard-WSL2",
    "cpuCount": 16,
    "cpuModel": "11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz",
    "totalMemory": "47.05 GB",
    "freeMemory": "37.14 GB"
  },
  "node": {
    "version": "v24.1.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.3.0"
  },
  "frameworks": [
    {
      "name": "vue",
      "version": "^3.5.22"
    },
    {
      "name": "nuxt",
      "version": "^4.1.2"
    }
  ],
  "databases": [
    {
      "name": "@prisma/client",
      "version": "6.16.3"
    }
  ],
  "betterAuth": {
    "version": "^1.3.26",
    "config": null
  }
}

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 @joshsedl on GitHub (Dec 12, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/6710 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce ## Problem Currently, when creating an invitation through "authClient.organization.inviteMember()", we can provide any string as the "role" parameter, even if this role doesn't exist (this problem might also exist for the "team" parameter, but we are currently not using "teams"). ## Steps to reproduce - Check your auth.ts and auth-client.ts and check that the `randomString` role doesn't exist. - Execute `authClient.organization.inviteMember()` with `role` set to `randomString`. - See that the Invitation is sent, even if the role is NOT valid. ## Proposed solution - Add role validation based on the roles set in auth.ts. ### Current vs. Expected behavior See above. ### What version of Better Auth are you using? 1.4.5 ### System info ```bash { "system": { "platform": "linux", "arch": "x64", "version": "#1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025", "release": "6.6.87.2-microsoft-standard-WSL2", "cpuCount": 16, "cpuModel": "11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz", "totalMemory": "47.05 GB", "freeMemory": "37.14 GB" }, "node": { "version": "v24.1.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.3.0" }, "frameworks": [ { "name": "vue", "version": "^3.5.22" }, { "name": "nuxt", "version": "^4.1.2" } ], "databases": [ { "name": "@prisma/client", "version": "6.16.3" } ], "betterAuth": { "version": "^1.3.26", "config": null } } ``` ### 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 lockedbug labels 2026-04-13 06:50:27 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Dec 12, 2025):

You're correct—currently, inviteMember does not validate if the provided role exists in the organization's configured roles. Any string can be passed as a role, and the invitation will be created without error, which can lead to invalid or inconsistent role assignments. In contrast, the team parameter is validated: if teams are enabled, the code checks that the provided team(s) exist and throws an error if not found, but no such check exists for roles (source).

Roles are defined by merging the default roles (admin, owner, member) with any custom roles you provide in your plugin options. This merged roles object is available in the plugin context and is used for permission checks elsewhere (source).

Best practice would be to validate the role parameter in inviteMember by checking that each provided role exists in the merged roles object (and, if using dynamic access control, also in the organization's roles in the database). This is how role validation is handled in other endpoints, such as dynamic access control routes (source).

How to implement role validation:
After parsing the roles from the request, add a check like:

const validRoles = Object.keys(ctx.context.orgOptions.roles || defaultRoles);
// If using dynamic roles, also query the OrganizationRole table for additional valid roles
const rolesToCheck = Array.isArray(ctx.body.role) ? ctx.body.role : [ctx.body.role];
for (const role of rolesToCheck) {
  if (!validRoles.includes(role)) {
    throw new APIError("BAD_REQUEST", {
      message: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND,
    });
  }
}

If you use dynamic roles, you should also query the OrganizationRole table for the organization and include those role names in validRoles.

This will ensure only valid, configured roles can be assigned via invitations, preventing accidental or malicious assignment of arbitrary roles.

There are open issues and discussions confirming this gap and recommending this validation (example). No merged pull requests currently address this, so adding this check would be a meaningful improvement.

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:3645947898 --> @dosubot[bot] commented on GitHub (Dec 12, 2025): <!-- Answer --> You're correct—currently, `inviteMember` does not validate if the provided `role` exists in the organization's configured roles. Any string can be passed as a role, and the invitation will be created without error, which can lead to invalid or inconsistent role assignments. In contrast, the `team` parameter is validated: if teams are enabled, the code checks that the provided team(s) exist and throws an error if not found, but no such check exists for roles [(source)](https://github.com/better-auth/better-auth/blob/6089a57dc7692f26cceaf1f26611bc5ff99f66d3/packages/better-auth/src/plugins/organization/routes/crud-invites.ts#L27-L463). Roles are defined by merging the default roles (`admin`, `owner`, `member`) with any custom roles you provide in your plugin options. This merged roles object is available in the plugin context and is used for permission checks elsewhere [(source)](https://github.com/better-auth/better-auth/blob/6089a57dc7692f26cceaf1f26611bc5ff99f66d3/packages/better-auth/src/plugins/organization/organization.ts#L71-L914). Best practice would be to validate the `role` parameter in `inviteMember` by checking that each provided role exists in the merged roles object (and, if using dynamic access control, also in the organization's roles in the database). This is how role validation is handled in other endpoints, such as dynamic access control routes [(source)](https://github.com/better-auth/better-auth/blob/6089a57dc7692f26cceaf1f26611bc5ff99f66d3/packages/better-auth/src/plugins/organization/routes/crud-access-control.ts#L24-L1229). **How to implement role validation:** After parsing the roles from the request, add a check like: ```typescript const validRoles = Object.keys(ctx.context.orgOptions.roles || defaultRoles); // If using dynamic roles, also query the OrganizationRole table for additional valid roles const rolesToCheck = Array.isArray(ctx.body.role) ? ctx.body.role : [ctx.body.role]; for (const role of rolesToCheck) { if (!validRoles.includes(role)) { throw new APIError("BAD_REQUEST", { message: ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND, }); } } ``` If you use dynamic roles, you should also query the `OrganizationRole` table for the organization and include those role names in `validRoles`. This will ensure only valid, configured roles can be assigned via invitations, preventing accidental or malicious assignment of arbitrary roles. There are open issues and discussions confirming this gap and recommending this validation [(example)](https://github.com/better-auth/better-auth/issues/6081). No merged pull requests currently address this, so adding this check would be a meaningful improvement. <!-- 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/a4171206-5b99-4f4c-b9d1-92793a429113?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/a4171206-5b99-4f4c-b9d1-92793a429113?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/a4171206-5b99-4f4c-b9d1-92793a429113?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/a4171206-5b99-4f4c-b9d1-92793a429113?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/a4171206-5b99-4f4c-b9d1-92793a429113?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/a4171206-5b99-4f4c-b9d1-92793a429113?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/a4171206-5b99-4f4c-b9d1-92793a429113?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=You%27re%20correct%E2%80%94currently%2C%20%60inviteMember%60%20does%20not%20validate%20if%20the%20provided%20%60role%60%20exists%20in%20the%20organization%27s%20configured%20roles.%20Any%20string%20can%20be%20passed%20as%20a%20role%2C%20and%20the%20invitation%20will%20be%20created%20without%20error%2C%20which%20can%20lead%20to%20invalid%20or%20inconsistent%20role%20assignments.%20In%20contrast%2C%20the%20%60team%60%20parameter%20is%20validated%3A%20if%20teams%20are%20enabled%2C%20the%20code%20checks%20that%20the%20provided%20team%28s%29%20exist%20and%20throws%20an%20error%20if%20not%20found%2C%20but%20no%20such%20check%20exists%20for%20roles%20%5B%28source%29%5D%28https%3A//github.com/better-auth/better-auth/blob/6089a57dc7692f26cceaf1f26611bc5ff99f66d3/packages/better-auth/src/plugins/organization/routes/crud-invites.ts%23L27-L463%29.%0A%0ARoles%20are%20defined%20by%20merging%20the%20default%20roles%20%28%60admin%60%2C%20%60owner%60%2C%20%60member%60%29%20with%20any%20custom%20roles%20you%20provide%20in%20your%20plugin%20options.%20This%20merged%20roles%20object%20is%20available%20in%20the%20plugin%20context%20and%20is%20used%20for%20permission%20checks%20elsewhere%20%5B%28source%29%5D%28https%3A//github.com/better-auth/better-auth/blob/6089a57dc7692f26cceaf1f26611bc5ff99f66d3/packages/better-auth/src/plugins/organization/organization.ts%23L71-L914%29.%0A%0ABest%20practice%20would%20be%20to%20validate%20the%20%60role%60%20parameter%20in%20%60inviteMember%60%20by%20checking%20that%20each%20provided%20role%20exists%20in%20the%20merged%20roles%20object%20%28and%2C%20if%20using%20dynamic%20access%20control%2C%20also%20in%20the%20organization%27s%20roles%20in%20the%20database%29.%20This%20is%20how%20role%20validation%20is%20handled%20in%20other%20endpoints%2C%20such%20as%20dynamic%20access%20control%20routes%20%5B%28source%29%5D%28https%3A//github.com/better-auth/better-auth/blob/6089a57dc7692f26cceaf1f26611bc5ff99f66d3/packages/better-auth/src/plugins/organization/routes/crud-access-control.ts%23L24-L1229%29.%0A%0A%2A%2AHow%20to%20implement%20role%20validation%3A%2A%2A%20%20%0AAfter%20parsing%20the%20roles%20from%20the%20request%2C%20add%20a%20check%20like%3A%0A%0A%60%60%60typescript%0Aconst%20validRoles%20%3D%20Object.keys%28ctx.context.orgOptions.roles%20%7C%7C%20defaultRoles%29%3B%0A//%20If%20using%20dynamic%20roles%2C%20also%20query%20the%20OrganizationRole%20table%20for%20additional%20valid%20roles%0Aconst%20rolesToCheck%20%3D%20Array.isArray%28ctx.body.role%29%20%3F%20ctx.body.role%20%3A%20%5Bctx.body.role%5D%3B%0Afor%20%28const%20role%20of%20rolesToCheck%29%20%7B%0A%20%20if%20%28%21validRoles.includes%28role%29%29%20%7B%0A%20%20%20%20throw%20new%20APIError%28%22BAD_REQUEST%22%2C%20%7B%0A%20%20%20%20%20%20message%3A%20ORGANIZATION_ERROR_CODES.ROLE_NOT_FOUND%2C%0A%20%20%20%20%7D%29%3B%0A%20%20%7D%0A%7D%0A%60%60%60%0AIf%20you%20use%20dynamic%20roles%2C%20you%20should%20also%20query%20the%20%60OrganizationRole%60%20table%20for%20the%20organization%20and%20include%20those%20role%20names%20in%20%60validRoles%60.%0A%0AThis%20will%20ensure%20only%20valid%2C%20configured%20roles%20can%20be%20assigned%20via%20invitations%2C%20preventing%20accidental%20or%20malicious%20assignment%20of%20arbitrary%20roles.%0A%0AThere%20are%20open%20issues%20and%20discussions%20confirming%20this%20gap%20and%20recommending%20this%20validation%20%5B%28example%29%5D%28https%3A//github.com/better-auth/better-auth/issues/6081%29.%20No%20merged%20pull%20requests%20currently%20address%20this%2C%20so%20adding%20this%20check%20would%20be%20a%20meaningful%20improvement.)&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/6710)
Author
Owner

@JPustkuchen commented on GitHub (Dec 12, 2025):

@dosu thanks for the workaround, but this should be solved within better-auth by design, I think.

<!-- gh-comment-id:3646615610 --> @JPustkuchen commented on GitHub (Dec 12, 2025): @dosu thanks for the workaround, but this should be solved within better-auth by design, I think.
Author
Owner

@GautamBytes commented on GitHub (Dec 15, 2025):

@dosu thanks for the workaround, but this should be solved within better-auth by design, I think.

Looking into it!

<!-- gh-comment-id:3655214328 --> @GautamBytes commented on GitHub (Dec 15, 2025): > [@dosu](https://github.com/dosu) thanks for the workaround, but this should be solved within better-auth by design, I think. Looking into it!
Author
Owner

@AVtheking commented on GitHub (Dec 15, 2025):

could we make this roles type safe too ?

<!-- gh-comment-id:3656106693 --> @AVtheking commented on GitHub (Dec 15, 2025): could we make this roles type safe too ?
Author
Owner

@joshsedl commented on GitHub (Dec 16, 2025):

Thanks for the fast fix!

<!-- gh-comment-id:3659793998 --> @joshsedl commented on GitHub (Dec 16, 2025): Thanks for the fast fix!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10597