@better-auth/sso 1.4.6-beta.2 → 1.4.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.
package/src/routes/sso.ts CHANGED
@@ -22,45 +22,19 @@ import type { IdentityProvider } from "samlify/types/src/entity-idp";
22
22
  import type { FlowResult } from "samlify/types/src/flow";
23
23
  import * as z from "zod/v4";
24
24
  import type { OIDCConfig, SAMLConfig, SSOOptions, SSOProvider } from "../types";
25
- import { validateEmailDomain } from "../utils";
26
-
27
- /**
28
- * Safely parses a value that might be a JSON string or already a parsed object
29
- * This handles cases where ORMs like Drizzle might return already parsed objects
30
- * instead of JSON strings from TEXT/JSON columns
31
- */
32
- function safeJsonParse<T>(value: string | T | null | undefined): T | null {
33
- if (!value) return null;
34
-
35
- // If it's already an object (not a string), return it as-is
36
- if (typeof value === "object") {
37
- return value as T;
38
- }
39
-
40
- // If it's a string, try to parse it
41
- if (typeof value === "string") {
42
- try {
43
- return JSON.parse(value) as T;
44
- } catch (error) {
45
- // If parsing fails, this might indicate the string is not valid JSON
46
- throw new Error(
47
- `Failed to parse JSON: ${error instanceof Error ? error.message : "Unknown error"}`,
48
- );
49
- }
50
- }
25
+ import { safeJsonParse, validateEmailDomain } from "../utils";
51
26
 
52
- return null;
53
- }
27
+ const spMetadataQuerySchema = z.object({
28
+ providerId: z.string(),
29
+ format: z.enum(["xml", "json"]).default("xml"),
30
+ });
54
31
 
55
32
  export const spMetadata = () => {
56
33
  return createAuthEndpoint(
57
34
  "/sso/saml2/sp/metadata",
58
35
  {
59
36
  method: "GET",
60
- query: z.object({
61
- providerId: z.string(),
62
- format: z.enum(["xml", "json"]).default("xml"),
63
- }),
37
+ query: spMetadataQuerySchema,
64
38
  metadata: {
65
39
  openapi: {
66
40
  operationId: "getSSOServiceProviderMetadata",
@@ -128,213 +102,211 @@ export const spMetadata = () => {
128
102
  );
129
103
  };
130
104
 
131
- export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
132
- return createAuthEndpoint(
133
- "/sso/register",
134
- {
135
- method: "POST",
136
- body: z.object({
137
- providerId: z.string({}).meta({
138
- description:
139
- "The ID of the provider. This is used to identify the provider during login and callback",
140
- }),
141
- issuer: z.string({}).meta({
142
- description: "The issuer of the provider",
143
- }),
144
- domain: z.string({}).meta({
105
+ const ssoProviderBodySchema = z.object({
106
+ providerId: z.string({}).meta({
107
+ description:
108
+ "The ID of the provider. This is used to identify the provider during login and callback",
109
+ }),
110
+ issuer: z.string({}).meta({
111
+ description: "The issuer of the provider",
112
+ }),
113
+ domain: z.string({}).meta({
114
+ description: "The domain of the provider. This is used for email matching",
115
+ }),
116
+ oidcConfig: z
117
+ .object({
118
+ clientId: z.string({}).meta({
119
+ description: "The client ID",
120
+ }),
121
+ clientSecret: z.string({}).meta({
122
+ description: "The client secret",
123
+ }),
124
+ authorizationEndpoint: z
125
+ .string({})
126
+ .meta({
127
+ description: "The authorization endpoint",
128
+ })
129
+ .optional(),
130
+ tokenEndpoint: z
131
+ .string({})
132
+ .meta({
133
+ description: "The token endpoint",
134
+ })
135
+ .optional(),
136
+ userInfoEndpoint: z
137
+ .string({})
138
+ .meta({
139
+ description: "The user info endpoint",
140
+ })
141
+ .optional(),
142
+ tokenEndpointAuthentication: z
143
+ .enum(["client_secret_post", "client_secret_basic"])
144
+ .optional(),
145
+ jwksEndpoint: z
146
+ .string({})
147
+ .meta({
148
+ description: "The JWKS endpoint",
149
+ })
150
+ .optional(),
151
+ discoveryEndpoint: z.string().optional(),
152
+ scopes: z
153
+ .array(z.string(), {})
154
+ .meta({
145
155
  description:
146
- "The domain of the provider. This is used for email matching",
147
- }),
148
- oidcConfig: z
149
- .object({
150
- clientId: z.string({}).meta({
151
- description: "The client ID",
152
- }),
153
- clientSecret: z.string({}).meta({
154
- description: "The client secret",
155
- }),
156
- authorizationEndpoint: z
157
- .string({})
158
- .meta({
159
- description: "The authorization endpoint",
160
- })
161
- .optional(),
162
- tokenEndpoint: z
163
- .string({})
164
- .meta({
165
- description: "The token endpoint",
166
- })
167
- .optional(),
168
- userInfoEndpoint: z
169
- .string({})
170
- .meta({
171
- description: "The user info endpoint",
172
- })
173
- .optional(),
174
- tokenEndpointAuthentication: z
175
- .enum(["client_secret_post", "client_secret_basic"])
176
- .optional(),
177
- jwksEndpoint: z
178
- .string({})
179
- .meta({
180
- description: "The JWKS endpoint",
181
- })
182
- .optional(),
183
- discoveryEndpoint: z.string().optional(),
184
- scopes: z
185
- .array(z.string(), {})
186
- .meta({
187
- description:
188
- "The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']",
189
- })
190
- .optional(),
191
- pkce: z
192
- .boolean({})
193
- .meta({
194
- description: "Whether to use PKCE for the authorization flow",
195
- })
196
- .default(true)
197
- .optional(),
198
- mapping: z
199
- .object({
200
- id: z.string({}).meta({
201
- description: "Field mapping for user ID (defaults to 'sub')",
202
- }),
203
- email: z.string({}).meta({
204
- description: "Field mapping for email (defaults to 'email')",
156
+ "The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']",
157
+ })
158
+ .optional(),
159
+ pkce: z
160
+ .boolean({})
161
+ .meta({
162
+ description: "Whether to use PKCE for the authorization flow",
163
+ })
164
+ .default(true)
165
+ .optional(),
166
+ mapping: z
167
+ .object({
168
+ id: z.string({}).meta({
169
+ description: "Field mapping for user ID (defaults to 'sub')",
170
+ }),
171
+ email: z.string({}).meta({
172
+ description: "Field mapping for email (defaults to 'email')",
173
+ }),
174
+ emailVerified: z
175
+ .string({})
176
+ .meta({
177
+ description:
178
+ "Field mapping for email verification (defaults to 'email_verified')",
179
+ })
180
+ .optional(),
181
+ name: z.string({}).meta({
182
+ description: "Field mapping for name (defaults to 'name')",
183
+ }),
184
+ image: z
185
+ .string({})
186
+ .meta({
187
+ description: "Field mapping for image (defaults to 'picture')",
188
+ })
189
+ .optional(),
190
+ extraFields: z.record(z.string(), z.any()).optional(),
191
+ })
192
+ .optional(),
193
+ })
194
+ .optional(),
195
+ samlConfig: z
196
+ .object({
197
+ entryPoint: z.string({}).meta({
198
+ description: "The entry point of the provider",
199
+ }),
200
+ cert: z.string({}).meta({
201
+ description: "The certificate of the provider",
202
+ }),
203
+ callbackUrl: z.string({}).meta({
204
+ description: "The callback URL of the provider",
205
+ }),
206
+ audience: z.string().optional(),
207
+ idpMetadata: z
208
+ .object({
209
+ metadata: z.string().optional(),
210
+ entityID: z.string().optional(),
211
+ cert: z.string().optional(),
212
+ privateKey: z.string().optional(),
213
+ privateKeyPass: z.string().optional(),
214
+ isAssertionEncrypted: z.boolean().optional(),
215
+ encPrivateKey: z.string().optional(),
216
+ encPrivateKeyPass: z.string().optional(),
217
+ singleSignOnService: z
218
+ .array(
219
+ z.object({
220
+ Binding: z.string().meta({
221
+ description: "The binding type for the SSO service",
205
222
  }),
206
- emailVerified: z
207
- .string({})
208
- .meta({
209
- description:
210
- "Field mapping for email verification (defaults to 'email_verified')",
211
- })
212
- .optional(),
213
- name: z.string({}).meta({
214
- description: "Field mapping for name (defaults to 'name')",
223
+ Location: z.string().meta({
224
+ description: "The URL for the SSO service",
215
225
  }),
216
- image: z
217
- .string({})
218
- .meta({
219
- description:
220
- "Field mapping for image (defaults to 'picture')",
221
- })
222
- .optional(),
223
- extraFields: z.record(z.string(), z.any()).optional(),
224
- })
225
- .optional(),
226
- })
227
- .optional(),
228
- samlConfig: z
229
- .object({
230
- entryPoint: z.string({}).meta({
231
- description: "The entry point of the provider",
232
- }),
233
- cert: z.string({}).meta({
234
- description: "The certificate of the provider",
235
- }),
236
- callbackUrl: z.string({}).meta({
237
- description: "The callback URL of the provider",
238
- }),
239
- audience: z.string().optional(),
240
- idpMetadata: z
241
- .object({
242
- metadata: z.string().optional(),
243
- entityID: z.string().optional(),
244
- cert: z.string().optional(),
245
- privateKey: z.string().optional(),
246
- privateKeyPass: z.string().optional(),
247
- isAssertionEncrypted: z.boolean().optional(),
248
- encPrivateKey: z.string().optional(),
249
- encPrivateKeyPass: z.string().optional(),
250
- singleSignOnService: z
251
- .array(
252
- z.object({
253
- Binding: z.string().meta({
254
- description: "The binding type for the SSO service",
255
- }),
256
- Location: z.string().meta({
257
- description: "The URL for the SSO service",
258
- }),
259
- }),
260
- )
261
- .optional()
262
- .meta({
263
- description: "Single Sign-On service configuration",
264
- }),
265
- })
266
- .optional(),
267
- spMetadata: z.object({
268
- metadata: z.string().optional(),
269
- entityID: z.string().optional(),
270
- binding: z.string().optional(),
271
- privateKey: z.string().optional(),
272
- privateKeyPass: z.string().optional(),
273
- isAssertionEncrypted: z.boolean().optional(),
274
- encPrivateKey: z.string().optional(),
275
- encPrivateKeyPass: z.string().optional(),
226
+ }),
227
+ )
228
+ .optional()
229
+ .meta({
230
+ description: "Single Sign-On service configuration",
276
231
  }),
277
- wantAssertionsSigned: z.boolean().optional(),
278
- signatureAlgorithm: z.string().optional(),
279
- digestAlgorithm: z.string().optional(),
280
- identifierFormat: z.string().optional(),
281
- privateKey: z.string().optional(),
282
- decryptionPvk: z.string().optional(),
283
- additionalParams: z.record(z.string(), z.any()).optional(),
284
- mapping: z
285
- .object({
286
- id: z.string({}).meta({
287
- description:
288
- "Field mapping for user ID (defaults to 'nameID')",
289
- }),
290
- email: z.string({}).meta({
291
- description: "Field mapping for email (defaults to 'email')",
292
- }),
293
- emailVerified: z
294
- .string({})
295
- .meta({
296
- description: "Field mapping for email verification",
297
- })
298
- .optional(),
299
- name: z.string({}).meta({
300
- description:
301
- "Field mapping for name (defaults to 'displayName')",
302
- }),
303
- firstName: z
304
- .string({})
305
- .meta({
306
- description:
307
- "Field mapping for first name (defaults to 'givenName')",
308
- })
309
- .optional(),
310
- lastName: z
311
- .string({})
312
- .meta({
313
- description:
314
- "Field mapping for last name (defaults to 'surname')",
315
- })
316
- .optional(),
317
- extraFields: z.record(z.string(), z.any()).optional(),
318
- })
319
- .optional(),
320
- })
321
- .optional(),
322
- organizationId: z
323
- .string({})
324
- .meta({
325
- description:
326
- "If organization plugin is enabled, the organization id to link the provider to",
327
- })
328
- .optional(),
329
- overrideUserInfo: z
330
- .boolean({})
331
- .meta({
332
- description:
333
- "Override user info with the provider info. Defaults to false",
334
- })
335
- .default(false)
336
- .optional(),
232
+ })
233
+ .optional(),
234
+ spMetadata: z.object({
235
+ metadata: z.string().optional(),
236
+ entityID: z.string().optional(),
237
+ binding: z.string().optional(),
238
+ privateKey: z.string().optional(),
239
+ privateKeyPass: z.string().optional(),
240
+ isAssertionEncrypted: z.boolean().optional(),
241
+ encPrivateKey: z.string().optional(),
242
+ encPrivateKeyPass: z.string().optional(),
337
243
  }),
244
+ wantAssertionsSigned: z.boolean().optional(),
245
+ signatureAlgorithm: z.string().optional(),
246
+ digestAlgorithm: z.string().optional(),
247
+ identifierFormat: z.string().optional(),
248
+ privateKey: z.string().optional(),
249
+ decryptionPvk: z.string().optional(),
250
+ additionalParams: z.record(z.string(), z.any()).optional(),
251
+ mapping: z
252
+ .object({
253
+ id: z.string({}).meta({
254
+ description: "Field mapping for user ID (defaults to 'nameID')",
255
+ }),
256
+ email: z.string({}).meta({
257
+ description: "Field mapping for email (defaults to 'email')",
258
+ }),
259
+ emailVerified: z
260
+ .string({})
261
+ .meta({
262
+ description: "Field mapping for email verification",
263
+ })
264
+ .optional(),
265
+ name: z.string({}).meta({
266
+ description: "Field mapping for name (defaults to 'displayName')",
267
+ }),
268
+ firstName: z
269
+ .string({})
270
+ .meta({
271
+ description:
272
+ "Field mapping for first name (defaults to 'givenName')",
273
+ })
274
+ .optional(),
275
+ lastName: z
276
+ .string({})
277
+ .meta({
278
+ description:
279
+ "Field mapping for last name (defaults to 'surname')",
280
+ })
281
+ .optional(),
282
+ extraFields: z.record(z.string(), z.any()).optional(),
283
+ })
284
+ .optional(),
285
+ })
286
+ .optional(),
287
+ organizationId: z
288
+ .string({})
289
+ .meta({
290
+ description:
291
+ "If organization plugin is enabled, the organization id to link the provider to",
292
+ })
293
+ .optional(),
294
+ overrideUserInfo: z
295
+ .boolean({})
296
+ .meta({
297
+ description:
298
+ "Override user info with the provider info. Defaults to false",
299
+ })
300
+ .default(false)
301
+ .optional(),
302
+ });
303
+
304
+ export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
305
+ return createAuthEndpoint(
306
+ "/sso/register",
307
+ {
308
+ method: "POST",
309
+ body: ssoProviderBodySchema,
338
310
  use: [sessionMiddleware],
339
311
  metadata: {
340
312
  openapi: {
@@ -683,12 +655,12 @@ export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
683
655
 
684
656
  return ctx.json({
685
657
  ...provider,
686
- oidcConfig: JSON.parse(
658
+ oidcConfig: safeJsonParse<OIDCConfig>(
687
659
  provider.oidcConfig as unknown as string,
688
- ) as OIDCConfig,
689
- samlConfig: JSON.parse(
660
+ ),
661
+ samlConfig: safeJsonParse<SAMLConfig>(
690
662
  provider.samlConfig as unknown as string,
691
- ) as SAMLConfig,
663
+ ),
692
664
  redirectURI: `${ctx.context.baseURL}/sso/callback/${provider.providerId}`,
693
665
  ...(options?.domainVerification?.enabled ? { domainVerified } : {}),
694
666
  ...(options?.domainVerification?.enabled
@@ -699,76 +671,77 @@ export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
699
671
  );
700
672
  };
701
673
 
674
+ const signInSSOBodySchema = z.object({
675
+ email: z
676
+ .string({})
677
+ .meta({
678
+ description:
679
+ "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",
680
+ })
681
+ .optional(),
682
+ organizationSlug: z
683
+ .string({})
684
+ .meta({
685
+ description: "The slug of the organization to sign in with",
686
+ })
687
+ .optional(),
688
+ providerId: z
689
+ .string({})
690
+ .meta({
691
+ description:
692
+ "The ID of the provider to sign in with. This can be provided instead of email or issuer",
693
+ })
694
+ .optional(),
695
+ domain: z
696
+ .string({})
697
+ .meta({
698
+ description: "The domain of the provider.",
699
+ })
700
+ .optional(),
701
+ callbackURL: z.string({}).meta({
702
+ description: "The URL to redirect to after login",
703
+ }),
704
+ errorCallbackURL: z
705
+ .string({})
706
+ .meta({
707
+ description: "The URL to redirect to after login",
708
+ })
709
+ .optional(),
710
+ newUserCallbackURL: z
711
+ .string({})
712
+ .meta({
713
+ description: "The URL to redirect to after login if the user is new",
714
+ })
715
+ .optional(),
716
+ scopes: z
717
+ .array(z.string(), {})
718
+ .meta({
719
+ description: "Scopes to request from the provider.",
720
+ })
721
+ .optional(),
722
+ loginHint: z
723
+ .string({})
724
+ .meta({
725
+ description:
726
+ "Login hint to send to the identity provider (e.g., email or identifier). If supported, will be sent as 'login_hint'.",
727
+ })
728
+ .optional(),
729
+ requestSignUp: z
730
+ .boolean({})
731
+ .meta({
732
+ description:
733
+ "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider",
734
+ })
735
+ .optional(),
736
+ providerType: z.enum(["oidc", "saml"]).optional(),
737
+ });
738
+
702
739
  export const signInSSO = (options?: SSOOptions) => {
703
740
  return createAuthEndpoint(
704
741
  "/sign-in/sso",
705
742
  {
706
743
  method: "POST",
707
- body: z.object({
708
- email: z
709
- .string({})
710
- .meta({
711
- description:
712
- "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",
713
- })
714
- .optional(),
715
- organizationSlug: z
716
- .string({})
717
- .meta({
718
- description: "The slug of the organization to sign in with",
719
- })
720
- .optional(),
721
- providerId: z
722
- .string({})
723
- .meta({
724
- description:
725
- "The ID of the provider to sign in with. This can be provided instead of email or issuer",
726
- })
727
- .optional(),
728
- domain: z
729
- .string({})
730
- .meta({
731
- description: "The domain of the provider.",
732
- })
733
- .optional(),
734
- callbackURL: z.string({}).meta({
735
- description: "The URL to redirect to after login",
736
- }),
737
- errorCallbackURL: z
738
- .string({})
739
- .meta({
740
- description: "The URL to redirect to after login",
741
- })
742
- .optional(),
743
- newUserCallbackURL: z
744
- .string({})
745
- .meta({
746
- description:
747
- "The URL to redirect to after login if the user is new",
748
- })
749
- .optional(),
750
- scopes: z
751
- .array(z.string(), {})
752
- .meta({
753
- description: "Scopes to request from the provider.",
754
- })
755
- .optional(),
756
- loginHint: z
757
- .string({})
758
- .meta({
759
- description:
760
- "Login hint to send to the identity provider (e.g., email or identifier). If supported, will be sent as 'login_hint'.",
761
- })
762
- .optional(),
763
- requestSignUp: z
764
- .boolean({})
765
- .meta({
766
- description:
767
- "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider",
768
- })
769
- .optional(),
770
- providerType: z.enum(["oidc", "saml"]).optional(),
771
- }),
744
+ body: signInSSOBodySchema,
772
745
  metadata: {
773
746
  openapi: {
774
747
  operationId: "signInWithSSO",
@@ -1101,17 +1074,19 @@ export const signInSSO = (options?: SSOOptions) => {
1101
1074
  );
1102
1075
  };
1103
1076
 
1077
+ const callbackSSOQuerySchema = z.object({
1078
+ code: z.string().optional(),
1079
+ state: z.string(),
1080
+ error: z.string().optional(),
1081
+ error_description: z.string().optional(),
1082
+ });
1083
+
1104
1084
  export const callbackSSO = (options?: SSOOptions) => {
1105
1085
  return createAuthEndpoint(
1106
1086
  "/sso/callback/:providerId",
1107
1087
  {
1108
1088
  method: "GET",
1109
- query: z.object({
1110
- code: z.string().optional(),
1111
- state: z.string(),
1112
- error: z.string().optional(),
1113
- error_description: z.string().optional(),
1114
- }),
1089
+ query: callbackSSOQuerySchema,
1115
1090
  allowedMediaTypes: [
1116
1091
  "application/x-www-form-urlencoded",
1117
1092
  "application/json",
@@ -1374,6 +1349,11 @@ export const callbackSSO = (options?: SSOOptions) => {
1374
1349
  }/error?error=invalid_provider&error_description=missing_user_info`,
1375
1350
  );
1376
1351
  }
1352
+ const isTrustedProvider =
1353
+ "domainVerified" in provider &&
1354
+ (provider as { domainVerified?: boolean }).domainVerified === true &&
1355
+ validateEmailDomain(userInfo.email, provider.domain);
1356
+
1377
1357
  const linked = await handleOAuthUserInfo(ctx, {
1378
1358
  userInfo: {
1379
1359
  email: userInfo.email,
@@ -1397,6 +1377,7 @@ export const callbackSSO = (options?: SSOOptions) => {
1397
1377
  callbackURL,
1398
1378
  disableSignUp: options?.disableImplicitSignUp && !requestSignUp,
1399
1379
  overrideUserInfo: config.overrideUserInfo,
1380
+ isTrustedProvider,
1400
1381
  });
1401
1382
  if (linked.error) {
1402
1383
  throw ctx.redirect(
@@ -1468,15 +1449,17 @@ export const callbackSSO = (options?: SSOOptions) => {
1468
1449
  );
1469
1450
  };
1470
1451
 
1452
+ const callbackSSOSAMLBodySchema = z.object({
1453
+ SAMLResponse: z.string(),
1454
+ RelayState: z.string().optional(),
1455
+ });
1456
+
1471
1457
  export const callbackSSOSAML = (options?: SSOOptions) => {
1472
1458
  return createAuthEndpoint(
1473
1459
  "/sso/saml2/callback/:providerId",
1474
1460
  {
1475
1461
  method: "POST",
1476
- body: z.object({
1477
- SAMLResponse: z.string(),
1478
- RelayState: z.string().optional(),
1479
- }),
1462
+ body: callbackSSOSAMLBodySchema,
1480
1463
  metadata: {
1481
1464
  isAction: false,
1482
1465
  allowedMediaTypes: [
@@ -1717,6 +1700,35 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
1717
1700
  });
1718
1701
 
1719
1702
  if (existingUser) {
1703
+ const account = await ctx.context.adapter.findOne<Account>({
1704
+ model: "account",
1705
+ where: [
1706
+ { field: "userId", value: existingUser.id },
1707
+ { field: "providerId", value: provider.providerId },
1708
+ { field: "accountId", value: userInfo.id },
1709
+ ],
1710
+ });
1711
+ if (!account) {
1712
+ const isTrustedProvider =
1713
+ ctx.context.options.account?.accountLinking?.trustedProviders?.includes(
1714
+ provider.providerId,
1715
+ ) ||
1716
+ ("domainVerified" in provider &&
1717
+ provider.domainVerified &&
1718
+ validateEmailDomain(userInfo.email, provider.domain));
1719
+ if (!isTrustedProvider) {
1720
+ const redirectUrl =
1721
+ RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
1722
+ throw ctx.redirect(`${redirectUrl}?error=account_not_linked`);
1723
+ }
1724
+ await ctx.context.internalAdapter.createAccount({
1725
+ userId: existingUser.id,
1726
+ providerId: provider.providerId,
1727
+ accountId: userInfo.id,
1728
+ accessToken: "",
1729
+ refreshToken: "",
1730
+ });
1731
+ }
1720
1732
  user = existingUser;
1721
1733
  } else {
1722
1734
  // if implicit sign up is disabled, we should not create a new user nor a new account.
@@ -1732,19 +1744,6 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
1732
1744
  name: userInfo.name,
1733
1745
  emailVerified: userInfo.emailVerified,
1734
1746
  });
1735
- }
1736
-
1737
- // Create or update account link
1738
- const account = await ctx.context.adapter.findOne<Account>({
1739
- model: "account",
1740
- where: [
1741
- { field: "userId", value: user.id },
1742
- { field: "providerId", value: provider.providerId },
1743
- { field: "accountId", value: userInfo.id },
1744
- ],
1745
- });
1746
-
1747
- if (!account) {
1748
1747
  await ctx.context.internalAdapter.createAccount({
1749
1748
  userId: user.id,
1750
1749
  providerId: provider.providerId,
@@ -1815,18 +1814,22 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
1815
1814
  );
1816
1815
  };
1817
1816
 
1817
+ const acsEndpointParamsSchema = z.object({
1818
+ providerId: z.string().optional(),
1819
+ });
1820
+
1821
+ const acsEndpointBodySchema = z.object({
1822
+ SAMLResponse: z.string(),
1823
+ RelayState: z.string().optional(),
1824
+ });
1825
+
1818
1826
  export const acsEndpoint = (options?: SSOOptions) => {
1819
1827
  return createAuthEndpoint(
1820
1828
  "/sso/saml2/sp/acs/:providerId",
1821
1829
  {
1822
1830
  method: "POST",
1823
- params: z.object({
1824
- providerId: z.string().optional(),
1825
- }),
1826
- body: z.object({
1827
- SAMLResponse: z.string(),
1828
- RelayState: z.string().optional(),
1829
- }),
1831
+ params: acsEndpointParamsSchema,
1832
+ body: acsEndpointBodySchema,
1830
1833
  metadata: {
1831
1834
  isAction: false,
1832
1835
  allowedMediaTypes: [