@better-auth/sso 1.4.0-beta.6 → 1.4.0-beta.7

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,17 +1,17 @@
1
1
 
2
- > @better-auth/sso@1.4.0-beta.6 build /home/runner/work/better-auth/better-auth/packages/sso
2
+ > @better-auth/sso@1.4.0-beta.7 build /home/runner/work/better-auth/better-auth/packages/sso
3
3
  > unbuild
4
4
 
5
5
  [info] Automatically detected entries: src/index, src/client [esm] [cjs] [dts]
6
6
  [info] Building sso
7
7
  [success] Build succeeded for sso
8
- [log] dist/index.cjs (total size: 67.2 kB, chunk size: 67.2 kB, exports: sso)
8
+ [log] dist/index.cjs (total size: 69 kB, chunk size: 69 kB, exports: sso)
9
9
 
10
10
  [log] dist/client.cjs (total size: 141 B, chunk size: 141 B, exports: ssoClient)
11
11
 
12
- [log] dist/index.mjs (total size: 65.5 kB, chunk size: 65.5 kB, exports: sso)
12
+ [log] dist/index.mjs (total size: 67.3 kB, chunk size: 67.3 kB, exports: sso)
13
13
 
14
14
  [log] dist/client.mjs (total size: 117 B, chunk size: 117 B, exports: ssoClient)
15
15
 
16
- Σ Total dist size (byte size): 260 kB
16
+ Σ Total dist size (byte size): 263 kB
17
17
  [log]
package/dist/index.cjs CHANGED
@@ -36,6 +36,22 @@ const fastValidator = {
36
36
  }
37
37
  };
38
38
  saml__namespace.setSchemaValidator(fastValidator);
39
+ function safeJsonParse(value) {
40
+ if (!value) return null;
41
+ if (typeof value === "object") {
42
+ return value;
43
+ }
44
+ if (typeof value === "string") {
45
+ try {
46
+ return JSON.parse(value);
47
+ } catch (error) {
48
+ throw new Error(
49
+ `Failed to parse JSON: ${error instanceof Error ? error.message : "Unknown error"}`
50
+ );
51
+ }
52
+ }
53
+ return null;
54
+ }
39
55
  const sso = (options) => {
40
56
  return {
41
57
  id: "sso",
@@ -75,7 +91,14 @@ const sso = (options) => {
75
91
  message: "No provider found for the given providerId"
76
92
  });
77
93
  }
78
- const parsedSamlConfig = JSON.parse(provider.samlConfig);
94
+ const parsedSamlConfig = safeJsonParse(
95
+ provider.samlConfig
96
+ );
97
+ if (!parsedSamlConfig) {
98
+ throw new api.APIError("BAD_REQUEST", {
99
+ message: "Invalid SAML configuration"
100
+ });
101
+ }
79
102
  const sp = parsedSamlConfig.spMetadata.metadata ? saml__namespace.ServiceProvider({
80
103
  metadata: parsedSamlConfig.spMetadata.metadata
81
104
  }) : saml__namespace.SPMetadata({
@@ -448,6 +471,23 @@ const sso = (options) => {
448
471
  });
449
472
  }
450
473
  }
474
+ const existingProvider = await ctx.context.adapter.findOne({
475
+ model: "ssoProvider",
476
+ where: [
477
+ {
478
+ field: "providerId",
479
+ value: body.providerId
480
+ }
481
+ ]
482
+ });
483
+ if (existingProvider) {
484
+ ctx.context.logger.info(
485
+ `SSO provider creation attempt with existing providerId: ${body.providerId}`
486
+ );
487
+ throw new api.APIError("UNPROCESSABLE_ENTITY", {
488
+ message: "SSO provider with this providerId already exists"
489
+ });
490
+ }
451
491
  const provider = await ctx.context.adapter.create({
452
492
  model: "ssoProvider",
453
493
  data: {
@@ -667,8 +707,12 @@ const sso = (options) => {
667
707
  }
668
708
  return {
669
709
  ...res,
670
- oidcConfig: res.oidcConfig ? JSON.parse(res.oidcConfig) : void 0,
671
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
710
+ oidcConfig: res.oidcConfig ? safeJsonParse(
711
+ res.oidcConfig
712
+ ) || void 0 : void 0,
713
+ samlConfig: res.samlConfig ? safeJsonParse(
714
+ res.samlConfig
715
+ ) || void 0 : void 0
672
716
  };
673
717
  });
674
718
  }
@@ -701,7 +745,7 @@ const sso = (options) => {
701
745
  redirectURI,
702
746
  state: state.state,
703
747
  codeVerifier: provider.oidcConfig.pkce ? state.codeVerifier : void 0,
704
- scopes: ctx.body.scopes || [
748
+ scopes: ctx.body.scopes || provider.oidcConfig.scopes || [
705
749
  "openid",
706
750
  "email",
707
751
  "profile",
@@ -715,7 +759,14 @@ const sso = (options) => {
715
759
  });
716
760
  }
717
761
  if (provider.samlConfig) {
718
- const parsedSamlConfig = typeof provider.samlConfig === "object" ? provider.samlConfig : JSON.parse(provider.samlConfig);
762
+ const parsedSamlConfig = typeof provider.samlConfig === "object" ? provider.samlConfig : safeJsonParse(
763
+ provider.samlConfig
764
+ );
765
+ if (!parsedSamlConfig) {
766
+ throw new api.APIError("BAD_REQUEST", {
767
+ message: "Invalid SAML configuration"
768
+ });
769
+ }
719
770
  const sp = saml__namespace.ServiceProvider({
720
771
  metadata: parsedSamlConfig.spMetadata.metadata,
721
772
  allowCreate: true
@@ -811,7 +862,7 @@ const sso = (options) => {
811
862
  }
812
863
  return {
813
864
  ...res,
814
- oidcConfig: JSON.parse(res.oidcConfig)
865
+ oidcConfig: safeJsonParse(res.oidcConfig) || void 0
815
866
  };
816
867
  });
817
868
  }
@@ -1059,7 +1110,9 @@ const sso = (options) => {
1059
1110
  if (!res) return null;
1060
1111
  return {
1061
1112
  ...res,
1062
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
1113
+ samlConfig: res.samlConfig ? safeJsonParse(
1114
+ res.samlConfig
1115
+ ) || void 0 : void 0
1063
1116
  };
1064
1117
  });
1065
1118
  }
@@ -1068,25 +1121,30 @@ const sso = (options) => {
1068
1121
  message: "No provider found for the given providerId"
1069
1122
  });
1070
1123
  }
1071
- const parsedSamlConfig = JSON.parse(
1124
+ const parsedSamlConfig = safeJsonParse(
1072
1125
  provider.samlConfig
1073
1126
  );
1127
+ if (!parsedSamlConfig) {
1128
+ throw new api.APIError("BAD_REQUEST", {
1129
+ message: "Invalid SAML configuration"
1130
+ });
1131
+ }
1074
1132
  const idpData = parsedSamlConfig.idpMetadata;
1075
1133
  let idp = null;
1076
1134
  if (!idpData?.metadata) {
1077
1135
  idp = saml__namespace.IdentityProvider({
1078
- entityID: idpData.entityID || parsedSamlConfig.issuer,
1136
+ entityID: idpData?.entityID || parsedSamlConfig.issuer,
1079
1137
  singleSignOnService: [
1080
1138
  {
1081
1139
  Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1082
1140
  Location: parsedSamlConfig.entryPoint
1083
1141
  }
1084
1142
  ],
1085
- signingCert: idpData.cert || parsedSamlConfig.cert,
1143
+ signingCert: idpData?.cert || parsedSamlConfig.cert,
1086
1144
  wantAuthnRequestsSigned: parsedSamlConfig.wantAssertionsSigned || false,
1087
- isAssertionEncrypted: idpData.isAssertionEncrypted || false,
1088
- encPrivateKey: idpData.encPrivateKey,
1089
- encPrivateKeyPass: idpData.encPrivateKeyPass
1145
+ isAssertionEncrypted: idpData?.isAssertionEncrypted || false,
1146
+ encPrivateKey: idpData?.encPrivateKey,
1147
+ encPrivateKeyPass: idpData?.encPrivateKeyPass
1090
1148
  });
1091
1149
  } else {
1092
1150
  idp = saml__namespace.IdentityProvider({
@@ -1333,7 +1391,9 @@ const sso = (options) => {
1333
1391
  if (!res) return null;
1334
1392
  return {
1335
1393
  ...res,
1336
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
1394
+ samlConfig: res.samlConfig ? safeJsonParse(
1395
+ res.samlConfig
1396
+ ) || void 0 : void 0
1337
1397
  };
1338
1398
  });
1339
1399
  }
package/dist/index.mjs CHANGED
@@ -19,6 +19,22 @@ const fastValidator = {
19
19
  }
20
20
  };
21
21
  saml.setSchemaValidator(fastValidator);
22
+ function safeJsonParse(value) {
23
+ if (!value) return null;
24
+ if (typeof value === "object") {
25
+ return value;
26
+ }
27
+ if (typeof value === "string") {
28
+ try {
29
+ return JSON.parse(value);
30
+ } catch (error) {
31
+ throw new Error(
32
+ `Failed to parse JSON: ${error instanceof Error ? error.message : "Unknown error"}`
33
+ );
34
+ }
35
+ }
36
+ return null;
37
+ }
22
38
  const sso = (options) => {
23
39
  return {
24
40
  id: "sso",
@@ -58,7 +74,14 @@ const sso = (options) => {
58
74
  message: "No provider found for the given providerId"
59
75
  });
60
76
  }
61
- const parsedSamlConfig = JSON.parse(provider.samlConfig);
77
+ const parsedSamlConfig = safeJsonParse(
78
+ provider.samlConfig
79
+ );
80
+ if (!parsedSamlConfig) {
81
+ throw new APIError("BAD_REQUEST", {
82
+ message: "Invalid SAML configuration"
83
+ });
84
+ }
62
85
  const sp = parsedSamlConfig.spMetadata.metadata ? saml.ServiceProvider({
63
86
  metadata: parsedSamlConfig.spMetadata.metadata
64
87
  }) : saml.SPMetadata({
@@ -431,6 +454,23 @@ const sso = (options) => {
431
454
  });
432
455
  }
433
456
  }
457
+ const existingProvider = await ctx.context.adapter.findOne({
458
+ model: "ssoProvider",
459
+ where: [
460
+ {
461
+ field: "providerId",
462
+ value: body.providerId
463
+ }
464
+ ]
465
+ });
466
+ if (existingProvider) {
467
+ ctx.context.logger.info(
468
+ `SSO provider creation attempt with existing providerId: ${body.providerId}`
469
+ );
470
+ throw new APIError("UNPROCESSABLE_ENTITY", {
471
+ message: "SSO provider with this providerId already exists"
472
+ });
473
+ }
434
474
  const provider = await ctx.context.adapter.create({
435
475
  model: "ssoProvider",
436
476
  data: {
@@ -650,8 +690,12 @@ const sso = (options) => {
650
690
  }
651
691
  return {
652
692
  ...res,
653
- oidcConfig: res.oidcConfig ? JSON.parse(res.oidcConfig) : void 0,
654
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
693
+ oidcConfig: res.oidcConfig ? safeJsonParse(
694
+ res.oidcConfig
695
+ ) || void 0 : void 0,
696
+ samlConfig: res.samlConfig ? safeJsonParse(
697
+ res.samlConfig
698
+ ) || void 0 : void 0
655
699
  };
656
700
  });
657
701
  }
@@ -684,7 +728,7 @@ const sso = (options) => {
684
728
  redirectURI,
685
729
  state: state.state,
686
730
  codeVerifier: provider.oidcConfig.pkce ? state.codeVerifier : void 0,
687
- scopes: ctx.body.scopes || [
731
+ scopes: ctx.body.scopes || provider.oidcConfig.scopes || [
688
732
  "openid",
689
733
  "email",
690
734
  "profile",
@@ -698,7 +742,14 @@ const sso = (options) => {
698
742
  });
699
743
  }
700
744
  if (provider.samlConfig) {
701
- const parsedSamlConfig = typeof provider.samlConfig === "object" ? provider.samlConfig : JSON.parse(provider.samlConfig);
745
+ const parsedSamlConfig = typeof provider.samlConfig === "object" ? provider.samlConfig : safeJsonParse(
746
+ provider.samlConfig
747
+ );
748
+ if (!parsedSamlConfig) {
749
+ throw new APIError("BAD_REQUEST", {
750
+ message: "Invalid SAML configuration"
751
+ });
752
+ }
702
753
  const sp = saml.ServiceProvider({
703
754
  metadata: parsedSamlConfig.spMetadata.metadata,
704
755
  allowCreate: true
@@ -794,7 +845,7 @@ const sso = (options) => {
794
845
  }
795
846
  return {
796
847
  ...res,
797
- oidcConfig: JSON.parse(res.oidcConfig)
848
+ oidcConfig: safeJsonParse(res.oidcConfig) || void 0
798
849
  };
799
850
  });
800
851
  }
@@ -1042,7 +1093,9 @@ const sso = (options) => {
1042
1093
  if (!res) return null;
1043
1094
  return {
1044
1095
  ...res,
1045
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
1096
+ samlConfig: res.samlConfig ? safeJsonParse(
1097
+ res.samlConfig
1098
+ ) || void 0 : void 0
1046
1099
  };
1047
1100
  });
1048
1101
  }
@@ -1051,25 +1104,30 @@ const sso = (options) => {
1051
1104
  message: "No provider found for the given providerId"
1052
1105
  });
1053
1106
  }
1054
- const parsedSamlConfig = JSON.parse(
1107
+ const parsedSamlConfig = safeJsonParse(
1055
1108
  provider.samlConfig
1056
1109
  );
1110
+ if (!parsedSamlConfig) {
1111
+ throw new APIError("BAD_REQUEST", {
1112
+ message: "Invalid SAML configuration"
1113
+ });
1114
+ }
1057
1115
  const idpData = parsedSamlConfig.idpMetadata;
1058
1116
  let idp = null;
1059
1117
  if (!idpData?.metadata) {
1060
1118
  idp = saml.IdentityProvider({
1061
- entityID: idpData.entityID || parsedSamlConfig.issuer,
1119
+ entityID: idpData?.entityID || parsedSamlConfig.issuer,
1062
1120
  singleSignOnService: [
1063
1121
  {
1064
1122
  Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1065
1123
  Location: parsedSamlConfig.entryPoint
1066
1124
  }
1067
1125
  ],
1068
- signingCert: idpData.cert || parsedSamlConfig.cert,
1126
+ signingCert: idpData?.cert || parsedSamlConfig.cert,
1069
1127
  wantAuthnRequestsSigned: parsedSamlConfig.wantAssertionsSigned || false,
1070
- isAssertionEncrypted: idpData.isAssertionEncrypted || false,
1071
- encPrivateKey: idpData.encPrivateKey,
1072
- encPrivateKeyPass: idpData.encPrivateKeyPass
1128
+ isAssertionEncrypted: idpData?.isAssertionEncrypted || false,
1129
+ encPrivateKey: idpData?.encPrivateKey,
1130
+ encPrivateKeyPass: idpData?.encPrivateKeyPass
1073
1131
  });
1074
1132
  } else {
1075
1133
  idp = saml.IdentityProvider({
@@ -1316,7 +1374,9 @@ const sso = (options) => {
1316
1374
  if (!res) return null;
1317
1375
  return {
1318
1376
  ...res,
1319
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
1377
+ samlConfig: res.samlConfig ? safeJsonParse(
1378
+ res.samlConfig
1379
+ ) || void 0 : void 0
1320
1380
  };
1321
1381
  });
1322
1382
  }
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.0-beta.6",
4
+ "version": "1.4.0-beta.7",
5
5
  "main": "dist/index.cjs",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -44,7 +44,7 @@
44
44
  }
45
45
  },
46
46
  "dependencies": {
47
- "@better-fetch/fetch": "^1.1.18",
47
+ "@better-fetch/fetch": "1.1.18",
48
48
  "fast-xml-parser": "^5.2.5",
49
49
  "jose": "^6.1.0",
50
50
  "oauth2-mock-server": "^7.2.1",
@@ -58,10 +58,10 @@
58
58
  "body-parser": "^2.2.0",
59
59
  "express": "^5.1.0",
60
60
  "unbuild": "3.6.1",
61
- "better-auth": "^1.4.0-beta.6"
61
+ "better-auth": "^1.4.0-beta.7"
62
62
  },
63
63
  "peerDependencies": {
64
- "better-auth": "1.4.0-beta.6"
64
+ "better-auth": "1.4.0-beta.7"
65
65
  },
66
66
  "scripts": {
67
67
  "test": "vitest",
package/src/index.ts CHANGED
@@ -38,6 +38,34 @@ const fastValidator = {
38
38
 
39
39
  saml.setSchemaValidator(fastValidator);
40
40
 
41
+ /**
42
+ * Safely parses a value that might be a JSON string or already a parsed object
43
+ * This handles cases where ORMs like Drizzle might return already parsed objects
44
+ * instead of JSON strings from TEXT/JSON columns
45
+ */
46
+ function safeJsonParse<T>(value: string | T | null | undefined): T | null {
47
+ if (!value) return null;
48
+
49
+ // If it's already an object (not a string), return it as-is
50
+ if (typeof value === "object") {
51
+ return value as T;
52
+ }
53
+
54
+ // If it's a string, try to parse it
55
+ if (typeof value === "string") {
56
+ try {
57
+ return JSON.parse(value) as T;
58
+ } catch (error) {
59
+ // If parsing fails, this might indicate the string is not valid JSON
60
+ throw new Error(
61
+ `Failed to parse JSON: ${error instanceof Error ? error.message : "Unknown error"}`,
62
+ );
63
+ }
64
+ }
65
+
66
+ return null;
67
+ }
68
+
41
69
  export interface OIDCMapping {
42
70
  id?: string;
43
71
  email?: string;
@@ -269,7 +297,14 @@ export const sso = (options?: SSOOptions) => {
269
297
  });
270
298
  }
271
299
 
272
- const parsedSamlConfig: SAMLConfig = JSON.parse(provider.samlConfig);
300
+ const parsedSamlConfig = safeJsonParse<SAMLConfig>(
301
+ provider.samlConfig,
302
+ );
303
+ if (!parsedSamlConfig) {
304
+ throw new APIError("BAD_REQUEST", {
305
+ message: "Invalid SAML configuration",
306
+ });
307
+ }
273
308
  const sp = parsedSamlConfig.spMetadata.metadata
274
309
  ? saml.ServiceProvider({
275
310
  metadata: parsedSamlConfig.spMetadata.metadata,
@@ -745,6 +780,26 @@ export const sso = (options?: SSOOptions) => {
745
780
  });
746
781
  }
747
782
  }
783
+
784
+ const existingProvider = await ctx.context.adapter.findOne({
785
+ model: "ssoProvider",
786
+ where: [
787
+ {
788
+ field: "providerId",
789
+ value: body.providerId,
790
+ },
791
+ ],
792
+ });
793
+
794
+ if (existingProvider) {
795
+ ctx.context.logger.info(
796
+ `SSO provider creation attempt with existing providerId: ${body.providerId}`,
797
+ );
798
+ throw new APIError("UNPROCESSABLE_ENTITY", {
799
+ message: "SSO provider with this providerId already exists",
800
+ });
801
+ }
802
+
748
803
  const provider = await ctx.context.adapter.create<
749
804
  Record<string, any>,
750
805
  SSOProvider
@@ -1040,10 +1095,14 @@ export const sso = (options?: SSOOptions) => {
1040
1095
  return {
1041
1096
  ...res,
1042
1097
  oidcConfig: res.oidcConfig
1043
- ? JSON.parse(res.oidcConfig as unknown as string)
1098
+ ? safeJsonParse<OIDCConfig>(
1099
+ res.oidcConfig as unknown as string,
1100
+ ) || undefined
1044
1101
  : undefined,
1045
1102
  samlConfig: res.samlConfig
1046
- ? JSON.parse(res.samlConfig as unknown as string)
1103
+ ? safeJsonParse<SAMLConfig>(
1104
+ res.samlConfig as unknown as string,
1105
+ ) || undefined
1047
1106
  : undefined,
1048
1107
  };
1049
1108
  });
@@ -1080,12 +1139,13 @@ export const sso = (options?: SSOOptions) => {
1080
1139
  codeVerifier: provider.oidcConfig.pkce
1081
1140
  ? state.codeVerifier
1082
1141
  : undefined,
1083
- scopes: ctx.body.scopes || [
1084
- "openid",
1085
- "email",
1086
- "profile",
1087
- "offline_access",
1088
- ],
1142
+ scopes: ctx.body.scopes ||
1143
+ provider.oidcConfig.scopes || [
1144
+ "openid",
1145
+ "email",
1146
+ "profile",
1147
+ "offline_access",
1148
+ ],
1089
1149
  authorizationEndpoint: provider.oidcConfig.authorizationEndpoint!,
1090
1150
  });
1091
1151
  return ctx.json({
@@ -1094,10 +1154,17 @@ export const sso = (options?: SSOOptions) => {
1094
1154
  });
1095
1155
  }
1096
1156
  if (provider.samlConfig) {
1097
- const parsedSamlConfig: SAMLConfig =
1157
+ const parsedSamlConfig =
1098
1158
  typeof provider.samlConfig === "object"
1099
1159
  ? provider.samlConfig
1100
- : JSON.parse(provider.samlConfig as unknown as string);
1160
+ : safeJsonParse<SAMLConfig>(
1161
+ provider.samlConfig as unknown as string,
1162
+ );
1163
+ if (!parsedSamlConfig) {
1164
+ throw new APIError("BAD_REQUEST", {
1165
+ message: "Invalid SAML configuration",
1166
+ });
1167
+ }
1101
1168
  const sp = saml.ServiceProvider({
1102
1169
  metadata: parsedSamlConfig.spMetadata.metadata,
1103
1170
  allowCreate: true,
@@ -1206,7 +1273,8 @@ export const sso = (options?: SSOOptions) => {
1206
1273
  }
1207
1274
  return {
1208
1275
  ...res,
1209
- oidcConfig: JSON.parse(res.oidcConfig),
1276
+ oidcConfig:
1277
+ safeJsonParse<OIDCConfig>(res.oidcConfig) || undefined,
1210
1278
  } as SSOProvider;
1211
1279
  });
1212
1280
  }
@@ -1533,7 +1601,9 @@ export const sso = (options?: SSOOptions) => {
1533
1601
  return {
1534
1602
  ...res,
1535
1603
  samlConfig: res.samlConfig
1536
- ? JSON.parse(res.samlConfig as unknown as string)
1604
+ ? safeJsonParse<SAMLConfig>(
1605
+ res.samlConfig as unknown as string,
1606
+ ) || undefined
1537
1607
  : undefined,
1538
1608
  };
1539
1609
  });
@@ -1544,28 +1614,33 @@ export const sso = (options?: SSOOptions) => {
1544
1614
  message: "No provider found for the given providerId",
1545
1615
  });
1546
1616
  }
1547
- const parsedSamlConfig = JSON.parse(
1617
+ const parsedSamlConfig = safeJsonParse<SAMLConfig>(
1548
1618
  provider.samlConfig as unknown as string,
1549
1619
  );
1620
+ if (!parsedSamlConfig) {
1621
+ throw new APIError("BAD_REQUEST", {
1622
+ message: "Invalid SAML configuration",
1623
+ });
1624
+ }
1550
1625
  const idpData = parsedSamlConfig.idpMetadata;
1551
1626
  let idp: IdentityProvider | null = null;
1552
1627
 
1553
1628
  // Construct IDP with fallback to manual configuration
1554
1629
  if (!idpData?.metadata) {
1555
1630
  idp = saml.IdentityProvider({
1556
- entityID: idpData.entityID || parsedSamlConfig.issuer,
1631
+ entityID: idpData?.entityID || parsedSamlConfig.issuer,
1557
1632
  singleSignOnService: [
1558
1633
  {
1559
1634
  Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1560
1635
  Location: parsedSamlConfig.entryPoint,
1561
1636
  },
1562
1637
  ],
1563
- signingCert: idpData.cert || parsedSamlConfig.cert,
1638
+ signingCert: idpData?.cert || parsedSamlConfig.cert,
1564
1639
  wantAuthnRequestsSigned:
1565
1640
  parsedSamlConfig.wantAssertionsSigned || false,
1566
- isAssertionEncrypted: idpData.isAssertionEncrypted || false,
1567
- encPrivateKey: idpData.encPrivateKey,
1568
- encPrivateKeyPass: idpData.encPrivateKeyPass,
1641
+ isAssertionEncrypted: idpData?.isAssertionEncrypted || false,
1642
+ encPrivateKey: idpData?.encPrivateKey,
1643
+ encPrivateKeyPass: idpData?.encPrivateKeyPass,
1569
1644
  });
1570
1645
  } else {
1571
1646
  idp = saml.IdentityProvider({
@@ -1865,7 +1940,9 @@ export const sso = (options?: SSOOptions) => {
1865
1940
  return {
1866
1941
  ...res,
1867
1942
  samlConfig: res.samlConfig
1868
- ? JSON.parse(res.samlConfig as unknown as string)
1943
+ ? safeJsonParse<SAMLConfig>(
1944
+ res.samlConfig as unknown as string,
1945
+ ) || undefined
1869
1946
  : undefined,
1870
1947
  };
1871
1948
  });
package/src/oidc.test.ts CHANGED
@@ -157,6 +157,43 @@ describe("SSO", async () => {
157
157
  }
158
158
  });
159
159
 
160
+ it("should not allow creating a provider with duplicate providerId", async () => {
161
+ const { headers } = await signInWithTestUser();
162
+
163
+ await auth.api.registerSSOProvider({
164
+ body: {
165
+ issuer: server.issuer.url!,
166
+ domain: "duplicate.com",
167
+ providerId: "duplicate-oidc-provider",
168
+ oidcConfig: {
169
+ clientId: "test",
170
+ clientSecret: "test",
171
+ },
172
+ },
173
+ headers,
174
+ });
175
+
176
+ await expect(
177
+ auth.api.registerSSOProvider({
178
+ body: {
179
+ issuer: server.issuer.url!,
180
+ domain: "another-duplicate.com",
181
+ providerId: "duplicate-oidc-provider",
182
+ oidcConfig: {
183
+ clientId: "test2",
184
+ clientSecret: "test2",
185
+ },
186
+ },
187
+ headers,
188
+ }),
189
+ ).rejects.toMatchObject({
190
+ status: "UNPROCESSABLE_ENTITY",
191
+ body: {
192
+ message: "SSO provider with this providerId already exists",
193
+ },
194
+ });
195
+ });
196
+
160
197
  it("should sign in with SSO provider with email matching", async () => {
161
198
  const headers = new Headers();
162
199
  const res = await authClient.signIn.sso({
package/src/saml.test.ts CHANGED
@@ -1069,4 +1069,53 @@ describe("SAML SSO", async () => {
1069
1069
  },
1070
1070
  });
1071
1071
  });
1072
+
1073
+ it("should not allow creating a provider with duplicate providerId", async () => {
1074
+ const headers = await getAuthHeaders();
1075
+ await authClient.signIn.email(testUser, {
1076
+ throw: true,
1077
+ onSuccess: setCookieToHeader(headers),
1078
+ });
1079
+
1080
+ await auth.api.registerSSOProvider({
1081
+ body: {
1082
+ providerId: "duplicate-provider",
1083
+ issuer: "http://localhost:8081",
1084
+ domain: "http://localhost:8081",
1085
+ samlConfig: {
1086
+ entryPoint: mockIdP.metadataUrl,
1087
+ cert: certificate,
1088
+ callbackUrl: "http://localhost:8081/api/sso/saml2/callback",
1089
+ spMetadata: {
1090
+ metadata: spMetadata,
1091
+ },
1092
+ },
1093
+ },
1094
+ headers,
1095
+ });
1096
+
1097
+ await expect(
1098
+ auth.api.registerSSOProvider({
1099
+ body: {
1100
+ providerId: "duplicate-provider",
1101
+ issuer: "http://localhost:8082",
1102
+ domain: "http://localhost:8082",
1103
+ samlConfig: {
1104
+ entryPoint: mockIdP.metadataUrl,
1105
+ cert: certificate,
1106
+ callbackUrl: "http://localhost:8082/api/sso/saml2/callback",
1107
+ spMetadata: {
1108
+ metadata: spMetadata,
1109
+ },
1110
+ },
1111
+ },
1112
+ headers,
1113
+ }),
1114
+ ).rejects.toMatchObject({
1115
+ status: "UNPROCESSABLE_ENTITY",
1116
+ body: {
1117
+ message: "SSO provider with this providerId already exists",
1118
+ },
1119
+ });
1120
+ });
1072
1121
  });