diff --git a/.changeset/tidy-maps-cheer.md b/.changeset/tidy-maps-cheer.md
new file mode 100644
index 0000000000..d5ebb2b6ab
--- /dev/null
+++ b/.changeset/tidy-maps-cheer.md
@@ -0,0 +1,5 @@
+---
+"better-auth": patch
+---
+
+Fix `organization.setActiveTeam` so it only accepts teams from the current active organization.
diff --git a/docs/content/docs/plugins/organization.mdx b/docs/content/docs/plugins/organization.mdx
index d1046f4c39..b24ae72e8c 100644
--- a/docs/content/docs/plugins/organization.mdx
+++ b/docs/content/docs/plugins/organization.mdx
@@ -1860,15 +1860,16 @@ Delete a team from an organization:
#### Set Active Team
-Sets the given team as the current active team. If `teamId` is `null` the current active team is unset.
+Sets the given team as the current active team for the current active organization. If `teamId` is `null` the current active team is unset.
```ts
type setActiveTeam = {
/**
* The team ID of the team to set as the current active team.
+ * The team must belong to the current active organization.
*/
- teamId?: string = "team-id"
+ teamId?: string | null = "team-id"
}
```
diff --git a/packages/better-auth/src/plugins/organization/routes/crud-team.ts b/packages/better-auth/src/plugins/organization/routes/crud-team.ts
index d8aa76e9e6..9cb96abaaf 100644
--- a/packages/better-auth/src/plugins/organization/routes/crud-team.ts
+++ b/packages/better-auth/src/plugins/organization/routes/crud-team.ts
@@ -679,7 +679,8 @@ export const setActiveTeam = (options: O) =>
use: [orgSessionMiddleware, orgMiddleware],
metadata: {
openapi: {
- description: "Set the active team",
+ description:
+ "Set the active team for the current active organization",
responses: {
"200": {
description: "Success",
@@ -734,7 +735,19 @@ export const setActiveTeam = (options: O) =>
teamId = ctx.body.teamId;
}
- const team = await adapter.findTeamById({ teamId });
+ const activeOrganizationId = session.session.activeOrganizationId;
+
+ if (!activeOrganizationId) {
+ throw APIError.from(
+ "BAD_REQUEST",
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION,
+ );
+ }
+
+ const team = await adapter.findTeamById({
+ teamId,
+ organizationId: activeOrganizationId,
+ });
if (!team) {
throw APIError.from(
diff --git a/packages/better-auth/src/plugins/organization/team.test.ts b/packages/better-auth/src/plugins/organization/team.test.ts
index 18c9d8af3a..01835e86e7 100644
--- a/packages/better-auth/src/plugins/organization/team.test.ts
+++ b/packages/better-auth/src/plugins/organization/team.test.ts
@@ -1,8 +1,10 @@
-import { describe, expect, it } from "vitest";
+import { beforeAll, describe, expect, it } from "vitest";
import { createAuthClient } from "../../client";
import { setCookieToHeader } from "../../cookies";
import { getTestInstance } from "../../test-utils/test-instance";
+import { isAPIError } from "../../utils/is-api-error";
import { organizationClient } from "./client";
+import { ORGANIZATION_ERROR_CODES } from "./error-codes";
import { organization } from "./organization";
describe("team", async () => {
@@ -500,6 +502,260 @@ describe("team", async () => {
});
});
+/**
+ * @see https://github.com/better-auth/better-auth/issues/9237
+ */
+describe("setActiveTeam org scoping", async () => {
+ const { auth, db, signInWithTestUser } = await getTestInstance({
+ plugins: [
+ organization({
+ async sendInvitationEmail() {},
+ teams: {
+ enabled: true,
+ },
+ }),
+ ],
+ logger: {
+ level: "error",
+ },
+ });
+
+ const { headers } = await signInWithTestUser();
+ const client = createAuthClient({
+ plugins: [
+ organizationClient({
+ teams: {
+ enabled: true,
+ },
+ }),
+ ],
+ baseURL: "http://localhost:3000/api/auth",
+ fetchOptions: {
+ customFetchImpl: async (url, init) => {
+ return auth.handler(new Request(url, init));
+ },
+ },
+ });
+
+ let activeOrganizationId: string;
+ let scopedTeamId: string;
+ let outOfScopeOrganizationId: string;
+ let outOfScopeTeamId: string;
+ let sessionToken: string;
+ let userId: string;
+
+ beforeAll(async () => {
+ const session = await client.getSession({
+ fetchOptions: { headers },
+ });
+ userId = session.data?.user.id as string;
+ sessionToken = session.data?.session.token as string;
+
+ const firstOrganization = await client.organization.create({
+ name: "Scoped Org One",
+ slug: "scoped-org-one",
+ fetchOptions: { headers },
+ });
+ activeOrganizationId = firstOrganization.data?.id as string;
+ expect(activeOrganizationId).toBeDefined();
+
+ const activeOrganizationTeam = await client.organization.createTeam(
+ {
+ name: "Scoped Team One",
+ organizationId: activeOrganizationId,
+ },
+ {
+ headers,
+ },
+ );
+ scopedTeamId = activeOrganizationTeam.data?.id as string;
+ expect(scopedTeamId).toBeDefined();
+
+ await auth.api.addTeamMember({
+ headers,
+ body: {
+ userId,
+ teamId: scopedTeamId,
+ organizationId: activeOrganizationId,
+ },
+ });
+
+ const secondOrganization = await client.organization.create({
+ name: "Scoped Org Two",
+ slug: "scoped-org-two",
+ keepCurrentActiveOrganization: true,
+ fetchOptions: { headers },
+ });
+ outOfScopeOrganizationId = secondOrganization.data?.id as string;
+ expect(outOfScopeOrganizationId).toBeDefined();
+
+ const outOfScopeTeam = await client.organization.createTeam(
+ {
+ name: "Scoped Team Two",
+ organizationId: outOfScopeOrganizationId,
+ },
+ {
+ headers,
+ },
+ );
+ outOfScopeTeamId = outOfScopeTeam.data?.id as string;
+ expect(outOfScopeTeamId).toBeDefined();
+
+ await auth.api.addTeamMember({
+ headers,
+ body: {
+ userId,
+ teamId: outOfScopeTeamId,
+ organizationId: outOfScopeOrganizationId,
+ },
+ });
+
+ const setActiveOrganization = await client.organization.setActive({
+ organizationId: activeOrganizationId,
+ fetchOptions: { headers },
+ });
+ expect(setActiveOrganization.error).toBeNull();
+
+ const setScopedTeam = await client.organization.setActiveTeam(
+ {
+ teamId: scopedTeamId,
+ },
+ {
+ headers,
+ },
+ );
+ expect(setScopedTeam.error).toBeNull();
+ expect(setScopedTeam.data?.id).toBe(scopedTeamId);
+
+ const sessionAfterScopedTeam = await client.getSession({
+ fetchOptions: { headers },
+ });
+ expect(
+ (sessionAfterScopedTeam.data?.session as any).activeOrganizationId,
+ ).toBe(activeOrganizationId);
+ expect((sessionAfterScopedTeam.data?.session as any).activeTeamId).toBe(
+ scopedTeamId,
+ );
+ });
+
+ it("should reject teams outside the active organization and preserve the active team", async () => {
+ const setOutOfScopeTeam = await client.organization.setActiveTeam(
+ {
+ teamId: outOfScopeTeamId,
+ },
+ {
+ headers,
+ },
+ );
+
+ expect(setOutOfScopeTeam.error?.status).toBe(400);
+ expect(setOutOfScopeTeam.error?.code).toContain(
+ ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND.code,
+ );
+ expect(setOutOfScopeTeam.data).toBeNull();
+
+ const sessionAfterRejectedTeam = await client.getSession({
+ fetchOptions: { headers },
+ });
+ expect(
+ (sessionAfterRejectedTeam.data?.session as any).activeOrganizationId,
+ ).toBe(activeOrganizationId);
+ expect((sessionAfterRejectedTeam.data?.session as any).activeTeamId).toBe(
+ scopedTeamId,
+ );
+ });
+
+ it("should reject refreshing the current active team when the session org changes externally", async () => {
+ await db.update({
+ model: "session",
+ where: [
+ {
+ field: "token",
+ value: sessionToken,
+ },
+ ],
+ update: {
+ activeOrganizationId: outOfScopeOrganizationId,
+ },
+ });
+
+ let error: unknown = null;
+ await auth.api
+ .setActiveTeam({
+ body: {},
+ headers,
+ })
+ .catch((e) => {
+ error = e;
+ });
+
+ expect(error).not.toBeNull();
+ expect(isAPIError(error)).toBeTruthy();
+ if (!isAPIError(error)) {
+ throw new Error("Expected setActiveTeam to throw an APIError");
+ }
+ expect(error.message).toBe(ORGANIZATION_ERROR_CODES.TEAM_NOT_FOUND.message);
+
+ const sessionAfterRefreshAttempt = await client.getSession({
+ fetchOptions: { headers },
+ });
+ expect(
+ (sessionAfterRefreshAttempt.data?.session as any).activeOrganizationId,
+ ).toBe(outOfScopeOrganizationId);
+ expect((sessionAfterRefreshAttempt.data?.session as any).activeTeamId).toBe(
+ scopedTeamId,
+ );
+
+ await db.update({
+ model: "session",
+ where: [
+ {
+ field: "token",
+ value: sessionToken,
+ },
+ ],
+ update: {
+ activeOrganizationId: activeOrganizationId,
+ },
+ });
+ });
+
+ it("should require an active organization before setting the active team", async () => {
+ expect(activeOrganizationId).toBeDefined();
+ expect(scopedTeamId).toBeDefined();
+ expect(userId).toBeDefined();
+
+ const unsetActiveOrganization = await client.organization.setActive({
+ organizationId: null,
+ fetchOptions: { headers },
+ });
+ expect(unsetActiveOrganization.error).toBeNull();
+
+ const result = await client.organization.setActiveTeam(
+ {
+ teamId: scopedTeamId,
+ },
+ {
+ headers,
+ },
+ );
+
+ expect(result.error?.status).toBe(400);
+ expect(result.error?.code).toContain(
+ ORGANIZATION_ERROR_CODES.NO_ACTIVE_ORGANIZATION.code,
+ );
+ expect(result.data).toBeNull();
+
+ const sessionAfterMissingOrganization = await client.getSession({
+ fetchOptions: { headers },
+ });
+ expect(
+ (sessionAfterMissingOrganization.data?.session as any)
+ .activeOrganizationId,
+ ).toBeNull();
+ });
+});
+
describe("multi team support", async () => {
const { auth, signInWithTestUser } = await getTestInstance(
{