@better-auth/sso 1.4.8-beta.3 → 1.4.8-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/dist/index.mjs +12 -6
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/linking/org-assignment.test.ts +325 -0
- package/src/linking/org-assignment.ts +13 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @better-auth/sso@1.4.8-beta.
|
|
2
|
+
> @better-auth/sso@1.4.8-beta.4 build /home/runner/work/better-auth/better-auth/packages/sso
|
|
3
3
|
> tsdown
|
|
4
4
|
|
|
5
5
|
[34mℹ[39m tsdown [2mv0.17.2[22m powered by rolldown [2mv1.0.0-beta.53[22m
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
[34mℹ[39m entry: [34msrc/index.ts, src/client.ts[39m
|
|
8
8
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
9
|
[34mℹ[39m Build start
|
|
10
|
-
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m92.
|
|
10
|
+
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m92.65 kB[22m [2m│ gzip: 18.13 kB[22m
|
|
11
11
|
[34mℹ[39m [2mdist/[22m[1mclient.mjs[22m [2m 0.15 kB[22m [2m│ gzip: 0.14 kB[22m
|
|
12
12
|
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 1.48 kB[22m [2m│ gzip: 0.51 kB[22m
|
|
13
13
|
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 0.49 kB[22m [2m│ gzip: 0.30 kB[22m
|
|
14
14
|
[34mℹ[39m [2mdist/[22m[32mindex-DNWhGQW-.d.mts[39m [2m42.86 kB[22m [2m│ gzip: 8.79 kB[22m
|
|
15
|
-
[34mℹ[39m 5 files, total: 137.
|
|
16
|
-
[32m✔[39m Build complete in [
|
|
15
|
+
[34mℹ[39m 5 files, total: 137.63 kB
|
|
16
|
+
[32m✔[39m Build complete in [32m11934ms[39m
|
package/dist/index.mjs
CHANGED
|
@@ -55,17 +55,22 @@ async function assignOrganizationFromProvider(ctx, options) {
|
|
|
55
55
|
* (e.g., Google OAuth with @acme.com email gets added to Acme's org).
|
|
56
56
|
*/
|
|
57
57
|
async function assignOrganizationByDomain(ctx, options) {
|
|
58
|
-
const { user, provisioningOptions } = options;
|
|
58
|
+
const { user, provisioningOptions, domainVerification } = options;
|
|
59
59
|
if (provisioningOptions?.disabled) return;
|
|
60
60
|
if (!ctx.context.options.plugins?.find((plugin) => plugin.id === "organization")) return;
|
|
61
61
|
const domain = user.email.split("@")[1];
|
|
62
62
|
if (!domain) return;
|
|
63
|
+
const whereClause = [{
|
|
64
|
+
field: "domain",
|
|
65
|
+
value: domain
|
|
66
|
+
}];
|
|
67
|
+
if (domainVerification?.enabled) whereClause.push({
|
|
68
|
+
field: "domainVerified",
|
|
69
|
+
value: true
|
|
70
|
+
});
|
|
63
71
|
const ssoProvider = await ctx.context.adapter.findOne({
|
|
64
72
|
model: "ssoProvider",
|
|
65
|
-
where:
|
|
66
|
-
field: "domain",
|
|
67
|
-
value: domain
|
|
68
|
-
}]
|
|
73
|
+
where: whereClause
|
|
69
74
|
});
|
|
70
75
|
if (!ssoProvider || !ssoProvider.organizationId) return;
|
|
71
76
|
if (await ctx.context.adapter.findOne({
|
|
@@ -2203,7 +2208,8 @@ function sso(options) {
|
|
|
2203
2208
|
if (!ctx.context.options.plugins?.find((plugin) => plugin.id === "organization")) return;
|
|
2204
2209
|
await assignOrganizationByDomain(ctx, {
|
|
2205
2210
|
user: newSession.user,
|
|
2206
|
-
provisioningOptions: options?.organizationProvisioning
|
|
2211
|
+
provisioningOptions: options?.organizationProvisioning,
|
|
2212
|
+
domainVerification: options?.domainVerification
|
|
2207
2213
|
});
|
|
2208
2214
|
})
|
|
2209
2215
|
}] },
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/sso",
|
|
3
3
|
"author": "Bereket Engida",
|
|
4
|
-
"version": "1.4.8-beta.
|
|
4
|
+
"version": "1.4.8-beta.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.mts",
|
|
@@ -66,10 +66,10 @@
|
|
|
66
66
|
"express": "^5.1.0",
|
|
67
67
|
"oauth2-mock-server": "^8.2.0",
|
|
68
68
|
"tsdown": "^0.17.2",
|
|
69
|
-
"better-auth": "1.4.8-beta.
|
|
69
|
+
"better-auth": "1.4.8-beta.4"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"better-auth": "1.4.8-beta.
|
|
72
|
+
"better-auth": "1.4.8-beta.4"
|
|
73
73
|
},
|
|
74
74
|
"scripts": {
|
|
75
75
|
"test": "vitest",
|
package/src/index.ts
CHANGED
|
@@ -156,6 +156,7 @@ export function sso<O extends SSOOptions>(options?: O | undefined): any {
|
|
|
156
156
|
await assignOrganizationByDomain(ctx, {
|
|
157
157
|
user: newSession.user,
|
|
158
158
|
provisioningOptions: options?.organizationProvisioning,
|
|
159
|
+
domainVerification: options?.domainVerification,
|
|
159
160
|
});
|
|
160
161
|
}),
|
|
161
162
|
},
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import type { GenericEndpointContext, User } from "better-auth";
|
|
2
|
+
import { betterAuth } from "better-auth";
|
|
3
|
+
import { memoryAdapter } from "better-auth/adapters/memory";
|
|
4
|
+
import { organization } from "better-auth/plugins";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { sso } from "..";
|
|
7
|
+
import { assignOrganizationByDomain } from "./org-assignment";
|
|
8
|
+
|
|
9
|
+
describe("assignOrganizationByDomain", () => {
|
|
10
|
+
const createTestContext = () => {
|
|
11
|
+
const data = {
|
|
12
|
+
user: [] as User[],
|
|
13
|
+
session: [] as { id: string }[],
|
|
14
|
+
account: [] as { id: string }[],
|
|
15
|
+
ssoProvider: [] as {
|
|
16
|
+
id: string;
|
|
17
|
+
providerId: string;
|
|
18
|
+
issuer: string;
|
|
19
|
+
domain: string;
|
|
20
|
+
domainVerified: boolean;
|
|
21
|
+
organizationId: string | null;
|
|
22
|
+
userId: string;
|
|
23
|
+
}[],
|
|
24
|
+
member: [] as {
|
|
25
|
+
id: string;
|
|
26
|
+
organizationId: string;
|
|
27
|
+
userId: string;
|
|
28
|
+
role: string;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
}[],
|
|
31
|
+
organization: [] as {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
slug: string;
|
|
35
|
+
createdAt: Date;
|
|
36
|
+
}[],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const memory = memoryAdapter(data);
|
|
40
|
+
|
|
41
|
+
const auth = betterAuth({
|
|
42
|
+
database: memory,
|
|
43
|
+
baseURL: "http://localhost:3000",
|
|
44
|
+
emailAndPassword: {
|
|
45
|
+
enabled: true,
|
|
46
|
+
},
|
|
47
|
+
plugins: [
|
|
48
|
+
sso({
|
|
49
|
+
domainVerification: {
|
|
50
|
+
enabled: true,
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
organization(),
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const createContext = async () => {
|
|
58
|
+
const context = await auth.$context;
|
|
59
|
+
return { context } as Partial<GenericEndpointContext>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return { auth, data, createContext };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const createUser = (overrides: Partial<User> = {}): User => ({
|
|
66
|
+
id: "user-1",
|
|
67
|
+
email: "alice@example.com",
|
|
68
|
+
name: "Alice",
|
|
69
|
+
emailVerified: true,
|
|
70
|
+
createdAt: new Date(),
|
|
71
|
+
updatedAt: new Date(),
|
|
72
|
+
...overrides,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const createOrg = (
|
|
76
|
+
overrides: Partial<{ id: string; name: string; slug: string }> = {},
|
|
77
|
+
) => ({
|
|
78
|
+
id: "org-1",
|
|
79
|
+
name: "Test Org",
|
|
80
|
+
slug: "test-org",
|
|
81
|
+
createdAt: new Date(),
|
|
82
|
+
...overrides,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const createProvider = (
|
|
86
|
+
overrides: Partial<{
|
|
87
|
+
id: string;
|
|
88
|
+
providerId: string;
|
|
89
|
+
issuer: string;
|
|
90
|
+
domain: string;
|
|
91
|
+
domainVerified: boolean;
|
|
92
|
+
organizationId: string | null;
|
|
93
|
+
userId: string;
|
|
94
|
+
}> = {},
|
|
95
|
+
) => ({
|
|
96
|
+
id: "provider-1",
|
|
97
|
+
providerId: "test-provider",
|
|
98
|
+
issuer: "https://idp.example.com",
|
|
99
|
+
domain: "example.com",
|
|
100
|
+
domainVerified: false,
|
|
101
|
+
organizationId: "org-1" as string | null,
|
|
102
|
+
userId: "user-1",
|
|
103
|
+
...overrides,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should NOT assign user to org when provider domain is unverified", async () => {
|
|
107
|
+
const { data, createContext } = createTestContext();
|
|
108
|
+
|
|
109
|
+
data.organization.push(createOrg());
|
|
110
|
+
data.ssoProvider.push(createProvider({ domainVerified: false }));
|
|
111
|
+
|
|
112
|
+
const user = createUser();
|
|
113
|
+
data.user.push(user);
|
|
114
|
+
|
|
115
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
116
|
+
await assignOrganizationByDomain(ctx, {
|
|
117
|
+
user,
|
|
118
|
+
domainVerification: { enabled: true },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
122
|
+
expect(members).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should assign user to org when provider domain is verified", async () => {
|
|
126
|
+
const { data, createContext } = createTestContext();
|
|
127
|
+
|
|
128
|
+
const org = createOrg();
|
|
129
|
+
data.organization.push(org);
|
|
130
|
+
data.ssoProvider.push(
|
|
131
|
+
createProvider({ domainVerified: true, organizationId: org.id }),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const user = createUser();
|
|
135
|
+
data.user.push(user);
|
|
136
|
+
|
|
137
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
138
|
+
await assignOrganizationByDomain(ctx, {
|
|
139
|
+
user,
|
|
140
|
+
domainVerification: { enabled: true },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
144
|
+
expect(members).toHaveLength(1);
|
|
145
|
+
expect(members[0]?.organizationId).toBe(org.id);
|
|
146
|
+
expect(members[0]?.role).toBe("member");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should NOT assign user when email domain does not match any provider", async () => {
|
|
150
|
+
const { data, createContext } = createTestContext();
|
|
151
|
+
|
|
152
|
+
data.organization.push(createOrg());
|
|
153
|
+
data.ssoProvider.push(createProvider({ domainVerified: true }));
|
|
154
|
+
|
|
155
|
+
const user = createUser({ email: "alice@other-domain.com" });
|
|
156
|
+
data.user.push(user);
|
|
157
|
+
|
|
158
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
159
|
+
await assignOrganizationByDomain(ctx, {
|
|
160
|
+
user,
|
|
161
|
+
domainVerification: { enabled: true },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
165
|
+
expect(members).toHaveLength(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should NOT assign user when provider has no organizationId", async () => {
|
|
169
|
+
const { data, createContext } = createTestContext();
|
|
170
|
+
|
|
171
|
+
data.ssoProvider.push(
|
|
172
|
+
createProvider({ domainVerified: true, organizationId: null }),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const user = createUser();
|
|
176
|
+
data.user.push(user);
|
|
177
|
+
|
|
178
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
179
|
+
await assignOrganizationByDomain(ctx, {
|
|
180
|
+
user,
|
|
181
|
+
domainVerification: { enabled: true },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
185
|
+
expect(members).toHaveLength(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should NOT assign user when provider has no domainVerified field (verification enabled)", async () => {
|
|
189
|
+
const { data, createContext } = createTestContext();
|
|
190
|
+
|
|
191
|
+
const org = createOrg();
|
|
192
|
+
data.organization.push(org);
|
|
193
|
+
|
|
194
|
+
data.ssoProvider.push({
|
|
195
|
+
id: "provider-1",
|
|
196
|
+
providerId: "test-provider",
|
|
197
|
+
issuer: "https://idp.example.com",
|
|
198
|
+
domain: "example.com",
|
|
199
|
+
organizationId: org.id,
|
|
200
|
+
userId: "user-1",
|
|
201
|
+
} as {
|
|
202
|
+
id: string;
|
|
203
|
+
providerId: string;
|
|
204
|
+
issuer: string;
|
|
205
|
+
domain: string;
|
|
206
|
+
domainVerified: boolean;
|
|
207
|
+
organizationId: string | null;
|
|
208
|
+
userId: string;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const user = createUser();
|
|
212
|
+
data.user.push(user);
|
|
213
|
+
|
|
214
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
215
|
+
await assignOrganizationByDomain(ctx, {
|
|
216
|
+
user,
|
|
217
|
+
domainVerification: { enabled: true },
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
221
|
+
expect(members).toHaveLength(0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should assign user when verification is disabled (no domainVerified check)", async () => {
|
|
225
|
+
const { data, createContext } = createTestContext();
|
|
226
|
+
|
|
227
|
+
const org = createOrg();
|
|
228
|
+
data.organization.push(org);
|
|
229
|
+
data.ssoProvider.push(
|
|
230
|
+
createProvider({ domainVerified: false, organizationId: org.id }),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const user = createUser();
|
|
234
|
+
data.user.push(user);
|
|
235
|
+
|
|
236
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
237
|
+
await assignOrganizationByDomain(ctx, {
|
|
238
|
+
user,
|
|
239
|
+
domainVerification: { enabled: false },
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
243
|
+
expect(members).toHaveLength(1);
|
|
244
|
+
expect(members[0]?.organizationId).toBe(org.id);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should NOT assign user when already a member of the org", async () => {
|
|
248
|
+
const { data, createContext } = createTestContext();
|
|
249
|
+
|
|
250
|
+
const org = createOrg();
|
|
251
|
+
data.organization.push(org);
|
|
252
|
+
data.ssoProvider.push(
|
|
253
|
+
createProvider({ domainVerified: true, organizationId: org.id }),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const user = createUser();
|
|
257
|
+
data.user.push(user);
|
|
258
|
+
|
|
259
|
+
data.member.push({
|
|
260
|
+
id: "member-1",
|
|
261
|
+
organizationId: org.id,
|
|
262
|
+
userId: user.id,
|
|
263
|
+
role: "admin",
|
|
264
|
+
createdAt: new Date(),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
268
|
+
await assignOrganizationByDomain(ctx, {
|
|
269
|
+
user,
|
|
270
|
+
domainVerification: { enabled: true },
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
274
|
+
expect(members).toHaveLength(1);
|
|
275
|
+
expect(members[0]?.role).toBe("admin");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should only find verified provider when multiple providers claim same domain", async () => {
|
|
279
|
+
const { data, createContext } = createTestContext();
|
|
280
|
+
|
|
281
|
+
const legitOrg = createOrg({
|
|
282
|
+
id: "legit-org",
|
|
283
|
+
name: "Legit Org",
|
|
284
|
+
slug: "legit-org",
|
|
285
|
+
});
|
|
286
|
+
const attackerOrg = createOrg({
|
|
287
|
+
id: "attacker-org",
|
|
288
|
+
name: "Attacker Org",
|
|
289
|
+
slug: "attacker-org",
|
|
290
|
+
});
|
|
291
|
+
data.organization.push(legitOrg, attackerOrg);
|
|
292
|
+
|
|
293
|
+
data.ssoProvider.push(
|
|
294
|
+
createProvider({
|
|
295
|
+
id: "attacker-provider",
|
|
296
|
+
providerId: "attacker-provider",
|
|
297
|
+
issuer: "https://attacker.com",
|
|
298
|
+
domainVerified: false,
|
|
299
|
+
organizationId: attackerOrg.id,
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
data.ssoProvider.push(
|
|
304
|
+
createProvider({
|
|
305
|
+
id: "legit-provider",
|
|
306
|
+
providerId: "legit-provider",
|
|
307
|
+
domainVerified: true,
|
|
308
|
+
organizationId: legitOrg.id,
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const user = createUser();
|
|
313
|
+
data.user.push(user);
|
|
314
|
+
|
|
315
|
+
const ctx = (await createContext()) as GenericEndpointContext;
|
|
316
|
+
await assignOrganizationByDomain(ctx, {
|
|
317
|
+
user,
|
|
318
|
+
domainVerification: { enabled: true },
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const members = data.member.filter((m) => m.userId === user.id);
|
|
322
|
+
expect(members).toHaveLength(1);
|
|
323
|
+
expect(members[0]?.organizationId).toBe(legitOrg.id);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -82,6 +82,9 @@ export async function assignOrganizationFromProvider(
|
|
|
82
82
|
export interface AssignOrganizationByDomainOptions {
|
|
83
83
|
user: User;
|
|
84
84
|
provisioningOptions?: OrganizationProvisioningOptions;
|
|
85
|
+
domainVerification?: {
|
|
86
|
+
enabled?: boolean;
|
|
87
|
+
};
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
/**
|
|
@@ -96,7 +99,7 @@ export async function assignOrganizationByDomain(
|
|
|
96
99
|
ctx: GenericEndpointContext,
|
|
97
100
|
options: AssignOrganizationByDomainOptions,
|
|
98
101
|
): Promise<void> {
|
|
99
|
-
const { user, provisioningOptions } = options;
|
|
102
|
+
const { user, provisioningOptions, domainVerification } = options;
|
|
100
103
|
|
|
101
104
|
if (provisioningOptions?.disabled) {
|
|
102
105
|
return;
|
|
@@ -115,11 +118,19 @@ export async function assignOrganizationByDomain(
|
|
|
115
118
|
return;
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
const whereClause: { field: string; value: string | boolean }[] = [
|
|
122
|
+
{ field: "domain", value: domain },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
if (domainVerification?.enabled) {
|
|
126
|
+
whereClause.push({ field: "domainVerified", value: true });
|
|
127
|
+
}
|
|
128
|
+
|
|
118
129
|
const ssoProvider = await ctx.context.adapter.findOne<
|
|
119
130
|
SSOProvider<SSOOptions>
|
|
120
131
|
>({
|
|
121
132
|
model: "ssoProvider",
|
|
122
|
-
where:
|
|
133
|
+
where: whereClause,
|
|
123
134
|
});
|
|
124
135
|
|
|
125
136
|
if (!ssoProvider || !ssoProvider.organizationId) {
|