@better-auth/sso 1.4.8-beta.3 → 1.4.8-beta.6

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @better-auth/sso@1.4.8-beta.3 build /home/runner/work/better-auth/better-auth/packages/sso
2
+ > @better-auth/sso@1.4.8-beta.6 build /home/runner/work/better-auth/better-auth/packages/sso
3
3
  > tsdown
4
4
 
5
5
  ℹ tsdown v0.17.2 powered by rolldown v1.0.0-beta.53
@@ -7,10 +7,10 @@
7
7
  ℹ entry: src/index.ts, src/client.ts
8
8
  ℹ tsconfig: tsconfig.json
9
9
  ℹ Build start
10
- ℹ dist/index.mjs 92.45 kB │ gzip: 18.08 kB
10
+ ℹ dist/index.mjs 92.65 kB │ gzip: 18.13 kB
11
11
  ℹ dist/client.mjs  0.15 kB │ gzip: 0.14 kB
12
12
  ℹ dist/index.d.mts  1.48 kB │ gzip: 0.51 kB
13
13
  ℹ dist/client.d.mts  0.49 kB │ gzip: 0.30 kB
14
- ℹ dist/index-DNWhGQW-.d.mts 42.86 kB │ gzip: 8.79 kB
15
- ℹ 5 files, total: 137.43 kB
16
- ✔ Build complete in 11758ms
14
+ ℹ dist/index-D6Q3ojGP.d.mts 42.86 kB │ gzip: 8.79 kB
15
+ ℹ 5 files, total: 137.63 kB
16
+ ✔ Build complete in 11966ms
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as SSOPlugin } from "./index-DNWhGQW-.mjs";
1
+ import { t as SSOPlugin } from "./index-D6Q3ojGP.mjs";
2
2
 
3
3
  //#region src/client.d.ts
4
4
  interface SSOClientOptions {
@@ -2,7 +2,7 @@ import { APIError } from "better-auth/api";
2
2
  import * as z$1 from "zod/v4";
3
3
  import z from "zod/v4";
4
4
  import { Awaitable, OAuth2Tokens, User } from "better-auth";
5
- import * as better_call0 from "better-call";
5
+ import * as better_call7 from "better-call";
6
6
 
7
7
  //#region src/saml/algorithms.d.ts
8
8
  declare const SignatureAlgorithm: {
@@ -371,7 +371,7 @@ interface SSOOptions {
371
371
  }
372
372
  //#endregion
373
373
  //#region src/routes/domain-verification.d.ts
374
- declare const requestDomainVerification: (options: SSOOptions) => better_call0.StrictEndpoint<"/sso/request-domain-verification", {
374
+ declare const requestDomainVerification: (options: SSOOptions) => better_call7.StrictEndpoint<"/sso/request-domain-verification", {
375
375
  method: "POST";
376
376
  body: z$1.ZodObject<{
377
377
  providerId: z$1.ZodString;
@@ -393,7 +393,7 @@ declare const requestDomainVerification: (options: SSOOptions) => better_call0.S
393
393
  };
394
394
  };
395
395
  };
396
- use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
396
+ use: ((inputContext: better_call7.MiddlewareInputContext<better_call7.MiddlewareOptions>) => Promise<{
397
397
  session: {
398
398
  session: Record<string, any> & {
399
399
  id: string;
@@ -419,7 +419,7 @@ declare const requestDomainVerification: (options: SSOOptions) => better_call0.S
419
419
  }, {
420
420
  domainVerificationToken: string;
421
421
  }>;
422
- declare const verifyDomain: (options: SSOOptions) => better_call0.StrictEndpoint<"/sso/verify-domain", {
422
+ declare const verifyDomain: (options: SSOOptions) => better_call7.StrictEndpoint<"/sso/verify-domain", {
423
423
  method: "POST";
424
424
  body: z$1.ZodObject<{
425
425
  providerId: z$1.ZodString;
@@ -444,7 +444,7 @@ declare const verifyDomain: (options: SSOOptions) => better_call0.StrictEndpoint
444
444
  };
445
445
  };
446
446
  };
447
- use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
447
+ use: ((inputContext: better_call7.MiddlewareInputContext<better_call7.MiddlewareOptions>) => Promise<{
448
448
  session: {
449
449
  session: Record<string, any> & {
450
450
  id: string;
@@ -488,7 +488,7 @@ interface SAMLConditions {
488
488
  * @throws {APIError} If timestamps are invalid, expired, or not yet valid
489
489
  */
490
490
  declare function validateSAMLTimestamp(conditions: SAMLConditions | undefined, options?: TimestampValidationOptions): void;
491
- declare const spMetadata: () => better_call0.StrictEndpoint<"/sso/saml2/sp/metadata", {
491
+ declare const spMetadata: () => better_call7.StrictEndpoint<"/sso/saml2/sp/metadata", {
492
492
  method: "GET";
493
493
  query: z.ZodObject<{
494
494
  providerId: z.ZodString;
@@ -510,7 +510,7 @@ declare const spMetadata: () => better_call0.StrictEndpoint<"/sso/saml2/sp/metad
510
510
  };
511
511
  };
512
512
  }, Response>;
513
- declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_call0.StrictEndpoint<"/sso/register", {
513
+ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_call7.StrictEndpoint<"/sso/register", {
514
514
  method: "POST";
515
515
  body: z.ZodObject<{
516
516
  providerId: z.ZodString;
@@ -589,7 +589,7 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
589
589
  organizationId: z.ZodOptional<z.ZodString>;
590
590
  overrideUserInfo: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
591
591
  }, z.core.$strip>;
592
- use: ((inputContext: better_call0.MiddlewareInputContext<better_call0.MiddlewareOptions>) => Promise<{
592
+ use: ((inputContext: better_call7.MiddlewareInputContext<better_call7.MiddlewareOptions>) => Promise<{
593
593
  session: {
594
594
  session: Record<string, any> & {
595
595
  id: string;
@@ -779,7 +779,7 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
779
779
  domainVerified: boolean;
780
780
  domainVerificationToken: string;
781
781
  } & SSOProvider<O> : SSOProvider<O>>;
782
- declare const signInSSO: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sign-in/sso", {
782
+ declare const signInSSO: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sign-in/sso", {
783
783
  method: "POST";
784
784
  body: z.ZodObject<{
785
785
  email: z.ZodOptional<z.ZodString>;
@@ -873,7 +873,7 @@ declare const signInSSO: (options?: SSOOptions) => better_call0.StrictEndpoint<"
873
873
  url: string;
874
874
  redirect: boolean;
875
875
  }>;
876
- declare const callbackSSO: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/callback/:providerId", {
876
+ declare const callbackSSO: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sso/callback/:providerId", {
877
877
  method: "GET";
878
878
  query: z.ZodObject<{
879
879
  code: z.ZodOptional<z.ZodString>;
@@ -896,7 +896,7 @@ declare const callbackSSO: (options?: SSOOptions) => better_call0.StrictEndpoint
896
896
  scope: "server";
897
897
  };
898
898
  }, never>;
899
- declare const callbackSSOSAML: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/saml2/callback/:providerId", {
899
+ declare const callbackSSOSAML: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sso/saml2/callback/:providerId", {
900
900
  method: "POST";
901
901
  body: z.ZodObject<{
902
902
  SAMLResponse: z.ZodString;
@@ -923,7 +923,7 @@ declare const callbackSSOSAML: (options?: SSOOptions) => better_call0.StrictEndp
923
923
  scope: "server";
924
924
  };
925
925
  }, never>;
926
- declare const acsEndpoint: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/saml2/sp/acs/:providerId", {
926
+ declare const acsEndpoint: (options?: SSOOptions) => better_call7.StrictEndpoint<"/sso/saml2/sp/acs/:providerId", {
927
927
  method: "POST";
928
928
  params: z.ZodObject<{
929
929
  providerId: z.ZodOptional<z.ZodString>;
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as KeyEncryptionAlgorithm, C as SAMLConfig, D as DataEncryptionAlgorithm, E as AlgorithmValidationOptions, O as DeprecatedAlgorithmBehavior, S as OIDCConfig, T as SSOProvider, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as TimestampValidationOptions, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as SignatureAlgorithm, k as DigestAlgorithm, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as SSOOptions, x as validateSAMLTimestamp, y as SAMLConditions } from "./index-DNWhGQW-.mjs";
1
+ import { A as KeyEncryptionAlgorithm, C as SAMLConfig, D as DataEncryptionAlgorithm, E as AlgorithmValidationOptions, O as DeprecatedAlgorithmBehavior, S as OIDCConfig, T as SSOProvider, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as TimestampValidationOptions, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as SignatureAlgorithm, k as DigestAlgorithm, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as SSOOptions, x as validateSAMLTimestamp, y as SAMLConditions } from "./index-D6Q3ojGP.mjs";
2
2
  export { AlgorithmValidationOptions, DataEncryptionAlgorithm, DeprecatedAlgorithmBehavior, DigestAlgorithm, DiscoverOIDCConfigParams, DiscoveryError, DiscoveryErrorCode, HydratedOIDCConfig, KeyEncryptionAlgorithm, OIDCConfig, OIDCDiscoveryDocument, REQUIRED_DISCOVERY_FIELDS, RequiredDiscoveryField, SAMLConditions, SAMLConfig, SSOOptions, SSOPlugin, SSOProvider, SignatureAlgorithm, TimestampValidationOptions, computeDiscoveryUrl, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
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.3",
4
+ "version": "1.4.8-beta.6",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
7
7
  "types": "dist/index.d.mts",
@@ -61,15 +61,15 @@
61
61
  "devDependencies": {
62
62
  "@types/body-parser": "^1.19.6",
63
63
  "@types/express": "^5.0.5",
64
- "better-call": "1.1.5",
64
+ "better-call": "1.1.7",
65
65
  "body-parser": "^2.2.1",
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.3"
69
+ "better-auth": "1.4.8-beta.6"
70
70
  },
71
71
  "peerDependencies": {
72
- "better-auth": "1.4.8-beta.3"
72
+ "better-auth": "1.4.8-beta.6"
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: [{ field: "domain", value: domain }],
133
+ where: whereClause,
123
134
  });
124
135
 
125
136
  if (!ssoProvider || !ssoProvider.organizationId) {