@better-auth/sso 1.3.0-beta.8 → 1.3.0

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/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { generateState } from 'better-auth';
2
2
  import { APIError, sessionMiddleware } from 'better-auth/api';
3
3
  import { parseState, validateAuthorizationCode, validateToken, handleOAuthUserInfo, createAuthorizationURL } from 'better-auth/oauth2';
4
4
  import { createAuthEndpoint } from 'better-auth/plugins';
5
- import { z } from 'zod';
5
+ import * as z from 'zod/v4';
6
6
  import * as saml from 'samlify';
7
7
  import { betterFetch, BetterFetchError } from '@better-fetch/fetch';
8
8
  import { decodeJwt } from 'jose';
@@ -74,47 +74,53 @@ const sso = (options) => {
74
74
  {
75
75
  method: "POST",
76
76
  body: z.object({
77
- providerId: z.string({
77
+ providerId: z.string({}).meta({
78
78
  description: "The ID of the provider. This is used to identify the provider during login and callback"
79
79
  }),
80
- issuer: z.string({
80
+ issuer: z.string({}).meta({
81
81
  description: "The issuer of the provider"
82
82
  }),
83
- domain: z.string({
83
+ domain: z.string({}).meta({
84
84
  description: "The domain of the provider. This is used for email matching"
85
85
  }),
86
86
  oidcConfig: z.object({
87
- clientId: z.string({
87
+ clientId: z.string({}).meta({
88
88
  description: "The client ID"
89
89
  }),
90
- clientSecret: z.string({
90
+ clientSecret: z.string({}).meta({
91
91
  description: "The client secret"
92
92
  }),
93
- authorizationEndpoint: z.string({
93
+ authorizationEndpoint: z.string({}).meta({
94
94
  description: "The authorization endpoint"
95
95
  }).optional(),
96
- tokenEndpoint: z.string({
96
+ tokenEndpoint: z.string({}).meta({
97
97
  description: "The token endpoint"
98
98
  }).optional(),
99
- userInfoEndpoint: z.string({
99
+ userInfoEndpoint: z.string({}).meta({
100
100
  description: "The user info endpoint"
101
101
  }).optional(),
102
102
  tokenEndpointAuthentication: z.enum(["client_secret_post", "client_secret_basic"]).optional(),
103
- jwksEndpoint: z.string({
103
+ jwksEndpoint: z.string({}).meta({
104
104
  description: "The JWKS endpoint"
105
105
  }).optional(),
106
106
  discoveryEndpoint: z.string().optional(),
107
- scopes: z.array(z.string(), {
107
+ scopes: z.array(z.string(), {}).meta({
108
108
  description: "The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']"
109
109
  }).optional(),
110
- pkce: z.boolean({
110
+ pkce: z.boolean({}).meta({
111
111
  description: "Whether to use PKCE for the authorization flow"
112
112
  }).default(true).optional()
113
113
  }).optional(),
114
114
  samlConfig: z.object({
115
- entryPoint: z.string(),
116
- cert: z.string(),
117
- callbackUrl: z.string(),
115
+ entryPoint: z.string({}).meta({
116
+ description: "The entry point of the provider"
117
+ }),
118
+ cert: z.string({}).meta({
119
+ description: "The certificate of the provider"
120
+ }),
121
+ callbackUrl: z.string({}).meta({
122
+ description: "The callback URL of the provider"
123
+ }),
118
124
  audience: z.string().optional(),
119
125
  idpMetadata: z.object({
120
126
  metadata: z.string(),
@@ -139,30 +145,30 @@ const sso = (options) => {
139
145
  identifierFormat: z.string().optional(),
140
146
  privateKey: z.string().optional(),
141
147
  decryptionPvk: z.string().optional(),
142
- additionalParams: z.record(z.string()).optional()
148
+ additionalParams: z.record(z.string(), z.any()).optional()
143
149
  }).optional(),
144
150
  mapping: z.object({
145
- id: z.string({
151
+ id: z.string({}).meta({
146
152
  description: "The field in the user info response that contains the id. Defaults to 'sub'"
147
153
  }),
148
- email: z.string({
154
+ email: z.string({}).meta({
149
155
  description: "The field in the user info response that contains the email. Defaults to 'email'"
150
156
  }),
151
- emailVerified: z.string({
157
+ emailVerified: z.string({}).meta({
152
158
  description: "The field in the user info response that contains whether the email is verified. defaults to 'email_verified'"
153
159
  }).optional(),
154
- name: z.string({
160
+ name: z.string({}).meta({
155
161
  description: "The field in the user info response that contains the name. Defaults to 'name'"
156
162
  }),
157
- image: z.string({
163
+ image: z.string({}).meta({
158
164
  description: "The field in the user info response that contains the image. Defaults to 'picture'"
159
165
  }).optional(),
160
- extraFields: z.record(z.string()).optional()
166
+ extraFields: z.record(z.string(), z.any()).optional()
161
167
  }).optional(),
162
- organizationId: z.string({
168
+ organizationId: z.string({}).meta({
163
169
  description: "If organization plugin is enabled, the organization id to link the provider to"
164
170
  }).optional(),
165
- overrideUserInfo: z.boolean({
171
+ overrideUserInfo: z.boolean({}).meta({
166
172
  description: "Override user info with the provider info. Defaults to false"
167
173
  }).default(false).optional()
168
174
  }),
@@ -333,6 +339,25 @@ const sso = (options) => {
333
339
  }
334
340
  },
335
341
  async (ctx) => {
342
+ const user = ctx.context.session?.user;
343
+ if (!user) {
344
+ throw new APIError("UNAUTHORIZED");
345
+ }
346
+ const limit = typeof options?.providersLimit === "function" ? await options.providersLimit(user) : options?.providersLimit ?? 10;
347
+ if (!limit) {
348
+ throw new APIError("FORBIDDEN", {
349
+ message: "SSO provider registration is disabled"
350
+ });
351
+ }
352
+ const providers = await ctx.context.adapter.findMany({
353
+ model: "ssoProvider",
354
+ where: [{ field: "userId", value: user.id }]
355
+ });
356
+ if (providers.length >= limit) {
357
+ throw new APIError("FORBIDDEN", {
358
+ message: "You have reached the maximum number of SSO providers"
359
+ });
360
+ }
336
361
  const body = ctx.body;
337
362
  const issuerValidator = z.string().url();
338
363
  if (issuerValidator.safeParse(body.issuer).error) {
@@ -398,31 +423,31 @@ const sso = (options) => {
398
423
  {
399
424
  method: "POST",
400
425
  body: z.object({
401
- email: z.string({
426
+ email: z.string({}).meta({
402
427
  description: "The email address to sign in with. This is used to identify the issuer to sign in with. It's optional if the issuer is provided"
403
428
  }).optional(),
404
- organizationSlug: z.string({
429
+ organizationSlug: z.string({}).meta({
405
430
  description: "The slug of the organization to sign in with"
406
431
  }).optional(),
407
- providerId: z.string({
432
+ providerId: z.string({}).meta({
408
433
  description: "The ID of the provider to sign in with. This can be provided instead of email or issuer"
409
434
  }).optional(),
410
- domain: z.string({
435
+ domain: z.string({}).meta({
411
436
  description: "The domain of the provider."
412
437
  }).optional(),
413
- callbackURL: z.string({
438
+ callbackURL: z.string({}).meta({
414
439
  description: "The URL to redirect to after login"
415
440
  }),
416
- errorCallbackURL: z.string({
441
+ errorCallbackURL: z.string({}).meta({
417
442
  description: "The URL to redirect to after login"
418
443
  }).optional(),
419
- newUserCallbackURL: z.string({
444
+ newUserCallbackURL: z.string({}).meta({
420
445
  description: "The URL to redirect to after login if the user is new"
421
446
  }).optional(),
422
- scopes: z.array(z.string(), {
447
+ scopes: z.array(z.string(), {}).meta({
423
448
  description: "Scopes to request from the provider."
424
449
  }).optional(),
425
- requestSignUp: z.boolean({
450
+ requestSignUp: z.boolean({}).meta({
426
451
  description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider"
427
452
  }).optional(),
428
453
  providerType: z.enum(["oidc", "saml"]).optional()
@@ -601,7 +626,9 @@ const sso = (options) => {
601
626
  });
602
627
  }
603
628
  return ctx.json({
604
- url: loginRequest.context,
629
+ url: `${loginRequest.context}&RelayState=${encodeURIComponent(
630
+ body.callbackURL
631
+ )}`,
605
632
  redirect: true
606
633
  });
607
634
  }
@@ -929,11 +956,11 @@ const sso = (options) => {
929
956
  extract.attributes[value]
930
957
  ])
931
958
  ),
932
- id: attributes[mapping.id || "nameID"],
933
- email: attributes[mapping.email || "nameID"],
959
+ id: attributes[mapping.id] || attributes["nameID"],
960
+ email: attributes[mapping.email] || attributes["nameID"] || attributes["email"],
934
961
  name: [
935
- attributes[mapping.firstName || "givenName"],
936
- attributes[mapping.lastName || "surname"]
962
+ attributes[mapping.firstName] || attributes["givenName"],
963
+ attributes[mapping.lastName] || attributes["surname"]
937
964
  ].filter(Boolean).join(" ") || parsedResponse.extract.attributes?.displayName,
938
965
  attributes: parsedResponse.extract.attributes
939
966
  };
@@ -999,10 +1026,9 @@ const sso = (options) => {
999
1026
  }
1000
1027
  let session = await ctx.context.internalAdapter.createSession(user.id, ctx);
1001
1028
  await setSessionCookie(ctx, { session, user });
1002
- return ctx.json({
1003
- redirect: true,
1004
- url: RelayState || `${parsedSamlConfig.issuer}/dashboard`
1005
- });
1029
+ throw ctx.redirect(
1030
+ RelayState || `${parsedSamlConfig.callbackUrl}` || `${parsedSamlConfig.issuer}`
1031
+ );
1006
1032
  }
1007
1033
  )
1008
1034
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@better-auth/sso",
3
3
  "author": "Bereket Engida",
4
- "version": "1.3.0-beta.8",
4
+ "version": "1.3.0",
5
5
  "main": "dist/index.cjs",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -47,7 +47,7 @@
47
47
  "oauth2-mock-server": "^7.2.0",
48
48
  "samlify": "^2.10.0",
49
49
  "zod": "^3.24.1",
50
- "better-auth": "^1.3.0-beta.8"
50
+ "better-auth": "^1.3.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/body-parser": "^1.19.6",
package/src/index.ts CHANGED
@@ -13,8 +13,9 @@ import {
13
13
  validateAuthorizationCode,
14
14
  validateToken,
15
15
  } from "better-auth/oauth2";
16
+
16
17
  import { createAuthEndpoint } from "better-auth/plugins";
17
- import { z } from "zod";
18
+ import * as z from "zod/v4";
18
19
  import * as saml from "samlify";
19
20
  import type { BindingContext } from "samlify/types/src/entity";
20
21
  import { betterFetch, BetterFetchError } from "@better-fetch/fetch";
@@ -132,6 +133,21 @@ export interface SSOOptions {
132
133
  * sign-in need to be called with with requestSignUp as true to create new users.
133
134
  */
134
135
  disableImplicitSignUp?: boolean;
136
+ /**
137
+ * Configure the maximum number of SSO providers a user can register.
138
+ * You can also pass a function that returns a number.
139
+ * Set to 0 to disable SSO provider registration.
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * providersLimit: async (user) => {
144
+ * const plan = await getUserPlan(user);
145
+ * return plan.name === "pro" ? 10 : 1;
146
+ * }
147
+ * ```
148
+ * @default 10
149
+ */
150
+ providersLimit?: number | ((user: User) => Promise<number> | number);
135
151
  }
136
152
 
137
153
  export const sso = (options?: SSOOptions) => {
@@ -192,37 +208,40 @@ export const sso = (options?: SSOOptions) => {
192
208
  {
193
209
  method: "POST",
194
210
  body: z.object({
195
- providerId: z.string({
211
+ providerId: z.string({}).meta({
196
212
  description:
197
213
  "The ID of the provider. This is used to identify the provider during login and callback",
198
214
  }),
199
- issuer: z.string({
215
+ issuer: z.string({}).meta({
200
216
  description: "The issuer of the provider",
201
217
  }),
202
- domain: z.string({
218
+ domain: z.string({}).meta({
203
219
  description:
204
220
  "The domain of the provider. This is used for email matching",
205
221
  }),
206
222
  oidcConfig: z
207
223
  .object({
208
- clientId: z.string({
224
+ clientId: z.string({}).meta({
209
225
  description: "The client ID",
210
226
  }),
211
- clientSecret: z.string({
227
+ clientSecret: z.string({}).meta({
212
228
  description: "The client secret",
213
229
  }),
214
230
  authorizationEndpoint: z
215
- .string({
231
+ .string({})
232
+ .meta({
216
233
  description: "The authorization endpoint",
217
234
  })
218
235
  .optional(),
219
236
  tokenEndpoint: z
220
- .string({
237
+ .string({})
238
+ .meta({
221
239
  description: "The token endpoint",
222
240
  })
223
241
  .optional(),
224
242
  userInfoEndpoint: z
225
- .string({
243
+ .string({})
244
+ .meta({
226
245
  description: "The user info endpoint",
227
246
  })
228
247
  .optional(),
@@ -230,19 +249,22 @@ export const sso = (options?: SSOOptions) => {
230
249
  .enum(["client_secret_post", "client_secret_basic"])
231
250
  .optional(),
232
251
  jwksEndpoint: z
233
- .string({
252
+ .string({})
253
+ .meta({
234
254
  description: "The JWKS endpoint",
235
255
  })
236
256
  .optional(),
237
257
  discoveryEndpoint: z.string().optional(),
238
258
  scopes: z
239
- .array(z.string(), {
259
+ .array(z.string(), {})
260
+ .meta({
240
261
  description:
241
262
  "The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']",
242
263
  })
243
264
  .optional(),
244
265
  pkce: z
245
- .boolean({
266
+ .boolean({})
267
+ .meta({
246
268
  description:
247
269
  "Whether to use PKCE for the authorization flow",
248
270
  })
@@ -252,9 +274,15 @@ export const sso = (options?: SSOOptions) => {
252
274
  .optional(),
253
275
  samlConfig: z
254
276
  .object({
255
- entryPoint: z.string(),
256
- cert: z.string(),
257
- callbackUrl: z.string(),
277
+ entryPoint: z.string({}).meta({
278
+ description: "The entry point of the provider",
279
+ }),
280
+ cert: z.string({}).meta({
281
+ description: "The certificate of the provider",
282
+ }),
283
+ callbackUrl: z.string({}).meta({
284
+ description: "The callback URL of the provider",
285
+ }),
258
286
  audience: z.string().optional(),
259
287
  idpMetadata: z
260
288
  .object({
@@ -282,46 +310,50 @@ export const sso = (options?: SSOOptions) => {
282
310
  identifierFormat: z.string().optional(),
283
311
  privateKey: z.string().optional(),
284
312
  decryptionPvk: z.string().optional(),
285
- additionalParams: z.record(z.string()).optional(),
313
+ additionalParams: z.record(z.string(), z.any()).optional(),
286
314
  })
287
315
  .optional(),
288
316
  mapping: z
289
317
  .object({
290
- id: z.string({
318
+ id: z.string({}).meta({
291
319
  description:
292
320
  "The field in the user info response that contains the id. Defaults to 'sub'",
293
321
  }),
294
- email: z.string({
322
+ email: z.string({}).meta({
295
323
  description:
296
324
  "The field in the user info response that contains the email. Defaults to 'email'",
297
325
  }),
298
326
  emailVerified: z
299
- .string({
327
+ .string({})
328
+ .meta({
300
329
  description:
301
330
  "The field in the user info response that contains whether the email is verified. defaults to 'email_verified'",
302
331
  })
303
332
  .optional(),
304
- name: z.string({
333
+ name: z.string({}).meta({
305
334
  description:
306
335
  "The field in the user info response that contains the name. Defaults to 'name'",
307
336
  }),
308
337
  image: z
309
- .string({
338
+ .string({})
339
+ .meta({
310
340
  description:
311
341
  "The field in the user info response that contains the image. Defaults to 'picture'",
312
342
  })
313
343
  .optional(),
314
- extraFields: z.record(z.string()).optional(),
344
+ extraFields: z.record(z.string(), z.any()).optional(),
315
345
  })
316
346
  .optional(),
317
347
  organizationId: z
318
- .string({
348
+ .string({})
349
+ .meta({
319
350
  description:
320
351
  "If organization plugin is enabled, the organization id to link the provider to",
321
352
  })
322
353
  .optional(),
323
354
  overrideUserInfo: z
324
- .boolean({
355
+ .boolean({})
356
+ .meta({
325
357
  description:
326
358
  "Override user info with the provider info. Defaults to false",
327
359
  })
@@ -509,6 +541,33 @@ export const sso = (options?: SSOOptions) => {
509
541
  },
510
542
  },
511
543
  async (ctx) => {
544
+ const user = ctx.context.session?.user;
545
+ if (!user) {
546
+ throw new APIError("UNAUTHORIZED");
547
+ }
548
+
549
+ const limit =
550
+ typeof options?.providersLimit === "function"
551
+ ? await options.providersLimit(user)
552
+ : options?.providersLimit ?? 10;
553
+
554
+ if (!limit) {
555
+ throw new APIError("FORBIDDEN", {
556
+ message: "SSO provider registration is disabled",
557
+ });
558
+ }
559
+
560
+ const providers = await ctx.context.adapter.findMany({
561
+ model: "ssoProvider",
562
+ where: [{ field: "userId", value: user.id }],
563
+ });
564
+
565
+ if (providers.length >= limit) {
566
+ throw new APIError("FORBIDDEN", {
567
+ message: "You have reached the maximum number of SSO providers",
568
+ });
569
+ }
570
+
512
571
  const body = ctx.body;
513
572
  const issuerValidator = z.string().url();
514
573
  if (issuerValidator.safeParse(body.issuer).error) {
@@ -589,48 +648,56 @@ export const sso = (options?: SSOOptions) => {
589
648
  method: "POST",
590
649
  body: z.object({
591
650
  email: z
592
- .string({
651
+ .string({})
652
+ .meta({
593
653
  description:
594
654
  "The email address to sign in with. This is used to identify the issuer to sign in with. It's optional if the issuer is provided",
595
655
  })
596
656
  .optional(),
597
657
  organizationSlug: z
598
- .string({
658
+ .string({})
659
+ .meta({
599
660
  description: "The slug of the organization to sign in with",
600
661
  })
601
662
  .optional(),
602
663
  providerId: z
603
- .string({
664
+ .string({})
665
+ .meta({
604
666
  description:
605
667
  "The ID of the provider to sign in with. This can be provided instead of email or issuer",
606
668
  })
607
669
  .optional(),
608
670
  domain: z
609
- .string({
671
+ .string({})
672
+ .meta({
610
673
  description: "The domain of the provider.",
611
674
  })
612
675
  .optional(),
613
- callbackURL: z.string({
676
+ callbackURL: z.string({}).meta({
614
677
  description: "The URL to redirect to after login",
615
678
  }),
616
679
  errorCallbackURL: z
617
- .string({
680
+ .string({})
681
+ .meta({
618
682
  description: "The URL to redirect to after login",
619
683
  })
620
684
  .optional(),
621
685
  newUserCallbackURL: z
622
- .string({
686
+ .string({})
687
+ .meta({
623
688
  description:
624
689
  "The URL to redirect to after login if the user is new",
625
690
  })
626
691
  .optional(),
627
692
  scopes: z
628
- .array(z.string(), {
693
+ .array(z.string(), {})
694
+ .meta({
629
695
  description: "Scopes to request from the provider.",
630
696
  })
631
697
  .optional(),
632
698
  requestSignUp: z
633
- .boolean({
699
+ .boolean({})
700
+ .meta({
634
701
  description:
635
702
  "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider",
636
703
  })
@@ -830,7 +897,9 @@ export const sso = (options?: SSOOptions) => {
830
897
  });
831
898
  }
832
899
  return ctx.json({
833
- url: loginRequest.context,
900
+ url: `${loginRequest.context}&RelayState=${encodeURIComponent(
901
+ body.callbackURL,
902
+ )}`,
834
903
  redirect: true,
835
904
  });
836
905
  }
@@ -1243,12 +1312,15 @@ export const sso = (options?: SSOOptions) => {
1243
1312
  extract.attributes[value as string],
1244
1313
  ]),
1245
1314
  ),
1246
- id: attributes[mapping.id || "nameID"],
1247
- email: attributes[mapping.email || "nameID" || "email"],
1315
+ id: attributes[mapping.id] || attributes["nameID"],
1316
+ email:
1317
+ attributes[mapping.email] ||
1318
+ attributes["nameID"] ||
1319
+ attributes["email"],
1248
1320
  name:
1249
1321
  [
1250
- attributes[mapping.firstName || "givenName"],
1251
- attributes[mapping.lastName || "surname"],
1322
+ attributes[mapping.firstName] || attributes["givenName"],
1323
+ attributes[mapping.lastName] || attributes["surname"],
1252
1324
  ]
1253
1325
  .filter(Boolean)
1254
1326
  .join(" ") || parsedResponse.extract.attributes?.displayName,
@@ -1328,10 +1400,11 @@ export const sso = (options?: SSOOptions) => {
1328
1400
  let session: Session =
1329
1401
  await ctx.context.internalAdapter.createSession(user.id, ctx);
1330
1402
  await setSessionCookie(ctx, { session, user });
1331
- return ctx.json({
1332
- redirect: true,
1333
- url: RelayState || `${parsedSamlConfig.issuer}/dashboard`,
1334
- });
1403
+ throw ctx.redirect(
1404
+ RelayState ||
1405
+ `${parsedSamlConfig.callbackUrl}` ||
1406
+ `${parsedSamlConfig.issuer}`,
1407
+ );
1335
1408
  },
1336
1409
  ),
1337
1410
  },
package/src/oidc.test.ts CHANGED
@@ -307,7 +307,108 @@ describe("SSO disable implicit sign in", async () => {
307
307
  userId: expect.any(String),
308
308
  });
309
309
  });
310
+ it("should not allow creating a provider if limit is set to 0", async () => {
311
+ const { auth, signInWithTestUser } = await getTestInstanceMemory({
312
+ plugins: [sso({ providersLimit: 0 })],
313
+ });
314
+ const { headers } = await signInWithTestUser();
315
+ await expect(
316
+ auth.api.registerSSOProvider({
317
+ body: {
318
+ issuer: server.issuer.url!,
319
+ domain: "localhost.com",
320
+ oidcConfig: {
321
+ clientId: "test",
322
+ clientSecret: "test",
323
+ },
324
+ providerId: "test",
325
+ },
326
+ headers,
327
+ }),
328
+ ).rejects.toMatchObject({
329
+ status: "FORBIDDEN",
330
+ body: { message: "SSO provider registration is disabled" },
331
+ });
332
+ });
333
+ it("should not allow creating a provider if limit is reached", async () => {
334
+ const { auth, signInWithTestUser } = await getTestInstanceMemory({
335
+ plugins: [sso({ providersLimit: 1 })],
336
+ });
337
+ const { headers } = await signInWithTestUser();
338
+
339
+ await auth.api.registerSSOProvider({
340
+ body: {
341
+ issuer: server.issuer.url!,
342
+ domain: "localhost.com",
343
+ oidcConfig: {
344
+ clientId: "test",
345
+ clientSecret: "test",
346
+ },
347
+ providerId: "test-1",
348
+ },
349
+ headers,
350
+ });
351
+
352
+ await expect(
353
+ auth.api.registerSSOProvider({
354
+ body: {
355
+ issuer: server.issuer.url!,
356
+ domain: "localhost.com",
357
+ oidcConfig: {
358
+ clientId: "test",
359
+ clientSecret: "test",
360
+ },
361
+ providerId: "test-2",
362
+ },
363
+ headers,
364
+ }),
365
+ ).rejects.toMatchObject({
366
+ status: "FORBIDDEN",
367
+ body: {
368
+ message: "You have reached the maximum number of SSO providers",
369
+ },
370
+ });
371
+ });
372
+
373
+ it("should not allow creating a provider if limit from function is reached", async () => {
374
+ const { auth, signInWithTestUser } = await getTestInstanceMemory({
375
+ plugins: [sso({ providersLimit: async () => 1 })],
376
+ });
377
+ const { headers } = await signInWithTestUser();
310
378
 
379
+ await auth.api.registerSSOProvider({
380
+ body: {
381
+ issuer: server.issuer.url!,
382
+ domain: "localhost.com",
383
+ oidcConfig: {
384
+ clientId: "test",
385
+ clientSecret: "test",
386
+ },
387
+ providerId: "test-1",
388
+ },
389
+ headers,
390
+ });
391
+
392
+ await expect(
393
+ auth.api.registerSSOProvider({
394
+ body: {
395
+ issuer: server.issuer.url!,
396
+ domain: "localhost.com",
397
+ oidcConfig: {
398
+ clientId: "test",
399
+ clientSecret: "test",
400
+ },
401
+ providerId: "test-2",
402
+ },
403
+ headers,
404
+ }),
405
+ ).rejects.toMatchObject({
406
+ status: "FORBIDDEN",
407
+ body: {
408
+ message: "You have reached the maximum number of SSO providers",
409
+ },
410
+ });
411
+ });
311
412
  it("should not create user with SSO provider when sign ups are disabled", async () => {
312
413
  const res = await auth.api.signInSSO({
313
414
  body: {