@better-auth/sso 1.3.0-beta.9 → 1.3.1-beta.1

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,9 +1026,8 @@ 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
- console.log("RelayState: ", RelayState);
1003
1029
  throw ctx.redirect(
1004
- RelayState || `${parsedSamlConfig.issuer}/dashboard`
1030
+ RelayState || `${parsedSamlConfig.callbackUrl}` || `${parsedSamlConfig.issuer}`
1005
1031
  );
1006
1032
  }
1007
1033
  )
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.9",
4
+ "version": "1.3.1-beta.1",
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.9"
50
+ "better-auth": "^1.3.1-beta.1"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/body-parser": "^1.19.6",
package/src/index.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  } from "better-auth/oauth2";
16
16
 
17
17
  import { createAuthEndpoint } from "better-auth/plugins";
18
- import { z } from "zod";
18
+ import * as z from "zod/v4";
19
19
  import * as saml from "samlify";
20
20
  import type { BindingContext } from "samlify/types/src/entity";
21
21
  import { betterFetch, BetterFetchError } from "@better-fetch/fetch";
@@ -133,6 +133,21 @@ export interface SSOOptions {
133
133
  * sign-in need to be called with with requestSignUp as true to create new users.
134
134
  */
135
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);
136
151
  }
137
152
 
138
153
  export const sso = (options?: SSOOptions) => {
@@ -193,37 +208,40 @@ export const sso = (options?: SSOOptions) => {
193
208
  {
194
209
  method: "POST",
195
210
  body: z.object({
196
- providerId: z.string({
211
+ providerId: z.string({}).meta({
197
212
  description:
198
213
  "The ID of the provider. This is used to identify the provider during login and callback",
199
214
  }),
200
- issuer: z.string({
215
+ issuer: z.string({}).meta({
201
216
  description: "The issuer of the provider",
202
217
  }),
203
- domain: z.string({
218
+ domain: z.string({}).meta({
204
219
  description:
205
220
  "The domain of the provider. This is used for email matching",
206
221
  }),
207
222
  oidcConfig: z
208
223
  .object({
209
- clientId: z.string({
224
+ clientId: z.string({}).meta({
210
225
  description: "The client ID",
211
226
  }),
212
- clientSecret: z.string({
227
+ clientSecret: z.string({}).meta({
213
228
  description: "The client secret",
214
229
  }),
215
230
  authorizationEndpoint: z
216
- .string({
231
+ .string({})
232
+ .meta({
217
233
  description: "The authorization endpoint",
218
234
  })
219
235
  .optional(),
220
236
  tokenEndpoint: z
221
- .string({
237
+ .string({})
238
+ .meta({
222
239
  description: "The token endpoint",
223
240
  })
224
241
  .optional(),
225
242
  userInfoEndpoint: z
226
- .string({
243
+ .string({})
244
+ .meta({
227
245
  description: "The user info endpoint",
228
246
  })
229
247
  .optional(),
@@ -231,19 +249,22 @@ export const sso = (options?: SSOOptions) => {
231
249
  .enum(["client_secret_post", "client_secret_basic"])
232
250
  .optional(),
233
251
  jwksEndpoint: z
234
- .string({
252
+ .string({})
253
+ .meta({
235
254
  description: "The JWKS endpoint",
236
255
  })
237
256
  .optional(),
238
257
  discoveryEndpoint: z.string().optional(),
239
258
  scopes: z
240
- .array(z.string(), {
259
+ .array(z.string(), {})
260
+ .meta({
241
261
  description:
242
262
  "The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']",
243
263
  })
244
264
  .optional(),
245
265
  pkce: z
246
- .boolean({
266
+ .boolean({})
267
+ .meta({
247
268
  description:
248
269
  "Whether to use PKCE for the authorization flow",
249
270
  })
@@ -253,9 +274,15 @@ export const sso = (options?: SSOOptions) => {
253
274
  .optional(),
254
275
  samlConfig: z
255
276
  .object({
256
- entryPoint: z.string(),
257
- cert: z.string(),
258
- 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
+ }),
259
286
  audience: z.string().optional(),
260
287
  idpMetadata: z
261
288
  .object({
@@ -283,46 +310,50 @@ export const sso = (options?: SSOOptions) => {
283
310
  identifierFormat: z.string().optional(),
284
311
  privateKey: z.string().optional(),
285
312
  decryptionPvk: z.string().optional(),
286
- additionalParams: z.record(z.string()).optional(),
313
+ additionalParams: z.record(z.string(), z.any()).optional(),
287
314
  })
288
315
  .optional(),
289
316
  mapping: z
290
317
  .object({
291
- id: z.string({
318
+ id: z.string({}).meta({
292
319
  description:
293
320
  "The field in the user info response that contains the id. Defaults to 'sub'",
294
321
  }),
295
- email: z.string({
322
+ email: z.string({}).meta({
296
323
  description:
297
324
  "The field in the user info response that contains the email. Defaults to 'email'",
298
325
  }),
299
326
  emailVerified: z
300
- .string({
327
+ .string({})
328
+ .meta({
301
329
  description:
302
330
  "The field in the user info response that contains whether the email is verified. defaults to 'email_verified'",
303
331
  })
304
332
  .optional(),
305
- name: z.string({
333
+ name: z.string({}).meta({
306
334
  description:
307
335
  "The field in the user info response that contains the name. Defaults to 'name'",
308
336
  }),
309
337
  image: z
310
- .string({
338
+ .string({})
339
+ .meta({
311
340
  description:
312
341
  "The field in the user info response that contains the image. Defaults to 'picture'",
313
342
  })
314
343
  .optional(),
315
- extraFields: z.record(z.string()).optional(),
344
+ extraFields: z.record(z.string(), z.any()).optional(),
316
345
  })
317
346
  .optional(),
318
347
  organizationId: z
319
- .string({
348
+ .string({})
349
+ .meta({
320
350
  description:
321
351
  "If organization plugin is enabled, the organization id to link the provider to",
322
352
  })
323
353
  .optional(),
324
354
  overrideUserInfo: z
325
- .boolean({
355
+ .boolean({})
356
+ .meta({
326
357
  description:
327
358
  "Override user info with the provider info. Defaults to false",
328
359
  })
@@ -510,6 +541,33 @@ export const sso = (options?: SSOOptions) => {
510
541
  },
511
542
  },
512
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
+
513
571
  const body = ctx.body;
514
572
  const issuerValidator = z.string().url();
515
573
  if (issuerValidator.safeParse(body.issuer).error) {
@@ -590,48 +648,56 @@ export const sso = (options?: SSOOptions) => {
590
648
  method: "POST",
591
649
  body: z.object({
592
650
  email: z
593
- .string({
651
+ .string({})
652
+ .meta({
594
653
  description:
595
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",
596
655
  })
597
656
  .optional(),
598
657
  organizationSlug: z
599
- .string({
658
+ .string({})
659
+ .meta({
600
660
  description: "The slug of the organization to sign in with",
601
661
  })
602
662
  .optional(),
603
663
  providerId: z
604
- .string({
664
+ .string({})
665
+ .meta({
605
666
  description:
606
667
  "The ID of the provider to sign in with. This can be provided instead of email or issuer",
607
668
  })
608
669
  .optional(),
609
670
  domain: z
610
- .string({
671
+ .string({})
672
+ .meta({
611
673
  description: "The domain of the provider.",
612
674
  })
613
675
  .optional(),
614
- callbackURL: z.string({
676
+ callbackURL: z.string({}).meta({
615
677
  description: "The URL to redirect to after login",
616
678
  }),
617
679
  errorCallbackURL: z
618
- .string({
680
+ .string({})
681
+ .meta({
619
682
  description: "The URL to redirect to after login",
620
683
  })
621
684
  .optional(),
622
685
  newUserCallbackURL: z
623
- .string({
686
+ .string({})
687
+ .meta({
624
688
  description:
625
689
  "The URL to redirect to after login if the user is new",
626
690
  })
627
691
  .optional(),
628
692
  scopes: z
629
- .array(z.string(), {
693
+ .array(z.string(), {})
694
+ .meta({
630
695
  description: "Scopes to request from the provider.",
631
696
  })
632
697
  .optional(),
633
698
  requestSignUp: z
634
- .boolean({
699
+ .boolean({})
700
+ .meta({
635
701
  description:
636
702
  "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider",
637
703
  })
@@ -831,7 +897,9 @@ export const sso = (options?: SSOOptions) => {
831
897
  });
832
898
  }
833
899
  return ctx.json({
834
- url: loginRequest.context,
900
+ url: `${loginRequest.context}&RelayState=${encodeURIComponent(
901
+ body.callbackURL,
902
+ )}`,
835
903
  redirect: true,
836
904
  });
837
905
  }
@@ -1244,12 +1312,15 @@ export const sso = (options?: SSOOptions) => {
1244
1312
  extract.attributes[value as string],
1245
1313
  ]),
1246
1314
  ),
1247
- id: attributes[mapping.id || "nameID"],
1248
- 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"],
1249
1320
  name:
1250
1321
  [
1251
- attributes[mapping.firstName || "givenName"],
1252
- attributes[mapping.lastName || "surname"],
1322
+ attributes[mapping.firstName] || attributes["givenName"],
1323
+ attributes[mapping.lastName] || attributes["surname"],
1253
1324
  ]
1254
1325
  .filter(Boolean)
1255
1326
  .join(" ") || parsedResponse.extract.attributes?.displayName,
@@ -1329,9 +1400,10 @@ export const sso = (options?: SSOOptions) => {
1329
1400
  let session: Session =
1330
1401
  await ctx.context.internalAdapter.createSession(user.id, ctx);
1331
1402
  await setSessionCookie(ctx, { session, user });
1332
- console.log("RelayState: ", RelayState);
1333
1403
  throw ctx.redirect(
1334
- RelayState || `${parsedSamlConfig.issuer}/dashboard`,
1404
+ RelayState ||
1405
+ `${parsedSamlConfig.callbackUrl}` ||
1406
+ `${parsedSamlConfig.issuer}`,
1335
1407
  );
1336
1408
  },
1337
1409
  ),
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: {