@better-auth/sso 1.3.23 → 1.3.25

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.3.23 build /home/runner/work/better-auth/better-auth/packages/sso
2
+ > @better-auth/sso@1.3.25 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
  }
@@ -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
  }
@@ -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.3.23",
4
+ "version": "1.3.25",
5
5
  "main": "dist/index.cjs",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -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.3.23"
61
+ "better-auth": "^1.3.25"
62
62
  },
63
63
  "peerDependencies": {
64
- "better-auth": "1.3.23"
64
+ "better-auth": "1.3.25"
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
  });
@@ -1094,10 +1153,17 @@ export const sso = (options?: SSOOptions) => {
1094
1153
  });
1095
1154
  }
1096
1155
  if (provider.samlConfig) {
1097
- const parsedSamlConfig: SAMLConfig =
1156
+ const parsedSamlConfig =
1098
1157
  typeof provider.samlConfig === "object"
1099
1158
  ? provider.samlConfig
1100
- : JSON.parse(provider.samlConfig as unknown as string);
1159
+ : safeJsonParse<SAMLConfig>(
1160
+ provider.samlConfig as unknown as string,
1161
+ );
1162
+ if (!parsedSamlConfig) {
1163
+ throw new APIError("BAD_REQUEST", {
1164
+ message: "Invalid SAML configuration",
1165
+ });
1166
+ }
1101
1167
  const sp = saml.ServiceProvider({
1102
1168
  metadata: parsedSamlConfig.spMetadata.metadata,
1103
1169
  allowCreate: true,
@@ -1206,7 +1272,8 @@ export const sso = (options?: SSOOptions) => {
1206
1272
  }
1207
1273
  return {
1208
1274
  ...res,
1209
- oidcConfig: JSON.parse(res.oidcConfig),
1275
+ oidcConfig:
1276
+ safeJsonParse<OIDCConfig>(res.oidcConfig) || undefined,
1210
1277
  } as SSOProvider;
1211
1278
  });
1212
1279
  }
@@ -1533,7 +1600,9 @@ export const sso = (options?: SSOOptions) => {
1533
1600
  return {
1534
1601
  ...res,
1535
1602
  samlConfig: res.samlConfig
1536
- ? JSON.parse(res.samlConfig as unknown as string)
1603
+ ? safeJsonParse<SAMLConfig>(
1604
+ res.samlConfig as unknown as string,
1605
+ ) || undefined
1537
1606
  : undefined,
1538
1607
  };
1539
1608
  });
@@ -1544,28 +1613,33 @@ export const sso = (options?: SSOOptions) => {
1544
1613
  message: "No provider found for the given providerId",
1545
1614
  });
1546
1615
  }
1547
- const parsedSamlConfig = JSON.parse(
1616
+ const parsedSamlConfig = safeJsonParse<SAMLConfig>(
1548
1617
  provider.samlConfig as unknown as string,
1549
1618
  );
1619
+ if (!parsedSamlConfig) {
1620
+ throw new APIError("BAD_REQUEST", {
1621
+ message: "Invalid SAML configuration",
1622
+ });
1623
+ }
1550
1624
  const idpData = parsedSamlConfig.idpMetadata;
1551
1625
  let idp: IdentityProvider | null = null;
1552
1626
 
1553
1627
  // Construct IDP with fallback to manual configuration
1554
1628
  if (!idpData?.metadata) {
1555
1629
  idp = saml.IdentityProvider({
1556
- entityID: idpData.entityID || parsedSamlConfig.issuer,
1630
+ entityID: idpData?.entityID || parsedSamlConfig.issuer,
1557
1631
  singleSignOnService: [
1558
1632
  {
1559
1633
  Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1560
1634
  Location: parsedSamlConfig.entryPoint,
1561
1635
  },
1562
1636
  ],
1563
- signingCert: idpData.cert || parsedSamlConfig.cert,
1637
+ signingCert: idpData?.cert || parsedSamlConfig.cert,
1564
1638
  wantAuthnRequestsSigned:
1565
1639
  parsedSamlConfig.wantAssertionsSigned || false,
1566
- isAssertionEncrypted: idpData.isAssertionEncrypted || false,
1567
- encPrivateKey: idpData.encPrivateKey,
1568
- encPrivateKeyPass: idpData.encPrivateKeyPass,
1640
+ isAssertionEncrypted: idpData?.isAssertionEncrypted || false,
1641
+ encPrivateKey: idpData?.encPrivateKey,
1642
+ encPrivateKeyPass: idpData?.encPrivateKeyPass,
1569
1643
  });
1570
1644
  } else {
1571
1645
  idp = saml.IdentityProvider({
@@ -1865,7 +1939,9 @@ export const sso = (options?: SSOOptions) => {
1865
1939
  return {
1866
1940
  ...res,
1867
1941
  samlConfig: res.samlConfig
1868
- ? JSON.parse(res.samlConfig as unknown as string)
1942
+ ? safeJsonParse<SAMLConfig>(
1943
+ res.samlConfig as unknown as string,
1944
+ ) || undefined
1869
1945
  : undefined,
1870
1946
  };
1871
1947
  });
package/src/oidc.test.ts CHANGED
@@ -1,18 +1,28 @@
1
1
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
2
+ import { getTestInstanceMemory as getTestInstance } from "better-auth/test";
2
3
  import { sso } from ".";
3
4
  import { OAuth2Server } from "oauth2-mock-server";
4
5
  import { betterFetch } from "@better-fetch/fetch";
5
- import { organization } from "better-auth/plugins/organization";
6
- import { getTestInstanceMemory } from "better-auth/test";
6
+ import { organization } from "better-auth/plugins";
7
+ import { createAuthClient } from "better-auth/client";
8
+ import { ssoClient } from "./client";
7
9
 
8
10
  let server = new OAuth2Server();
9
11
 
10
12
  describe("SSO", async () => {
11
- const { auth, signInWithTestUser, customFetchImpl } =
12
- await getTestInstanceMemory({
13
+ const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
14
+ await getTestInstance({
13
15
  plugins: [sso(), organization()],
14
16
  });
15
17
 
18
+ const authClient = createAuthClient({
19
+ plugins: [ssoClient()],
20
+ baseURL: "http://localhost:3000",
21
+ fetchOptions: {
22
+ customFetchImpl,
23
+ },
24
+ });
25
+
16
26
  beforeAll(async () => {
17
27
  await server.issuer.keys.generate("RS256");
18
28
  server.issuer.on;
@@ -57,7 +67,7 @@ describe("SSO", async () => {
57
67
  });
58
68
 
59
69
  if (!location) throw new Error("No redirect location found");
60
-
70
+ const newHeaders = new Headers();
61
71
  let callbackURL = "";
62
72
  await betterFetch(location, {
63
73
  method: "GET",
@@ -65,10 +75,11 @@ describe("SSO", async () => {
65
75
  headers,
66
76
  onError(context) {
67
77
  callbackURL = context.response.headers.get("location") || "";
78
+ cookieSetter(newHeaders)(context);
68
79
  },
69
80
  });
70
81
 
71
- return callbackURL;
82
+ return { callbackURL, headers: newHeaders };
72
83
  }
73
84
 
74
85
  it("should register a new SSO provider", async () => {
@@ -146,123 +157,114 @@ describe("SSO", async () => {
146
157
  }
147
158
  });
148
159
 
149
- it("should sign in with SSO provider with email matching", async () => {
150
- const res = await auth.api.signInSSO({
160
+ it("should not allow creating a provider with duplicate providerId", async () => {
161
+ const { headers } = await signInWithTestUser();
162
+
163
+ await auth.api.registerSSOProvider({
151
164
  body: {
152
- email: "my-email@localhost.com",
153
- callbackURL: "/dashboard",
165
+ issuer: server.issuer.url!,
166
+ domain: "duplicate.com",
167
+ providerId: "duplicate-oidc-provider",
168
+ oidcConfig: {
169
+ clientId: "test",
170
+ clientSecret: "test",
171
+ },
154
172
  },
173
+ headers,
155
174
  });
156
- expect(res.url).toContain("http://localhost:8080/authorize");
157
- expect(res.url).toContain(
158
- "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
159
- );
160
- const headers = new Headers();
161
- const callbackURL = await simulateOAuthFlow(res.url, headers);
162
- expect(callbackURL).toContain("/dashboard");
163
- });
164
175
 
165
- it("should sign in with SSO provider with domain", async () => {
166
- const res = await auth.api.signInSSO({
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",
167
191
  body: {
168
- email: "my-email@test.com",
169
- domain: "localhost.com",
170
- callbackURL: "/dashboard",
192
+ message: "SSO provider with this providerId already exists",
171
193
  },
172
194
  });
173
- expect(res.url).toContain("http://localhost:8080/authorize");
174
- expect(res.url).toContain(
175
- "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
176
- );
177
- const headers = new Headers();
178
- const callbackURL = await simulateOAuthFlow(res.url, headers);
179
- expect(callbackURL).toContain("/dashboard");
180
195
  });
181
196
 
182
- it("should sign in with SSO provider with providerId", async () => {
183
- const res = await auth.api.signInSSO({
184
- body: {
185
- providerId: "test",
186
- callbackURL: "/dashboard",
197
+ it("should sign in with SSO provider with email matching", async () => {
198
+ const headers = new Headers();
199
+ const res = await authClient.signIn.sso({
200
+ email: "my-email@localhost.com",
201
+ callbackURL: "/dashboard",
202
+ fetchOptions: {
203
+ throw: true,
204
+ onSuccess: cookieSetter(headers),
187
205
  },
188
206
  });
189
207
  expect(res.url).toContain("http://localhost:8080/authorize");
190
208
  expect(res.url).toContain(
191
209
  "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
192
210
  );
193
- const headers = new Headers();
194
- const callbackURL = await simulateOAuthFlow(res.url, headers);
211
+ const { callbackURL } = await simulateOAuthFlow(res.url, headers);
195
212
  expect(callbackURL).toContain("/dashboard");
196
213
  });
197
- });
198
-
199
- describe("SSO with defaultSSO array", async () => {
200
- const { auth, signInWithTestUser, customFetchImpl } =
201
- await getTestInstanceMemory({
202
- plugins: [
203
- sso({
204
- defaultSSO: [
205
- {
206
- domain: "localhost.com",
207
- providerId: "default-test",
208
- oidcConfig: {
209
- issuer: "http://localhost:8080",
210
- clientId: "test",
211
- clientSecret: "test",
212
- authorizationEndpoint: "http://localhost:8080/authorize",
213
- tokenEndpoint: "http://localhost:8080/token",
214
- jwksEndpoint: "http://localhost:8080/jwks",
215
- discoveryEndpoint:
216
- "http://localhost:8080/.well-known/openid-configuration",
217
- pkce: true,
218
- mapping: {
219
- id: "sub",
220
- email: "email",
221
- emailVerified: "email_verified",
222
- name: "name",
223
- image: "picture",
224
- },
225
- },
226
- },
227
- ],
228
- }),
229
- organization(),
230
- ],
231
- });
232
214
 
233
- it("should use default SSO provider from array when no provider found in database using providerId", async () => {
234
- const res = await auth.api.signInSSO({
235
- body: {
236
- providerId: "default-test",
237
- callbackURL: "/dashboard",
215
+ it("should sign in with SSO provider with domain", async () => {
216
+ const headers = new Headers();
217
+ const res = await authClient.signIn.sso({
218
+ email: "my-email@test.com",
219
+ domain: "localhost.com",
220
+ callbackURL: "/dashboard",
221
+ fetchOptions: {
222
+ throw: true,
223
+ onSuccess: cookieSetter(headers),
238
224
  },
239
225
  });
240
226
  expect(res.url).toContain("http://localhost:8080/authorize");
241
227
  expect(res.url).toContain(
242
- "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Fdefault-test",
228
+ "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
243
229
  );
230
+ const { callbackURL } = await simulateOAuthFlow(res.url, headers);
231
+ expect(callbackURL).toContain("/dashboard");
244
232
  });
245
233
 
246
- it("should use default SSO provider from array when no provider found in database using domain fallback", async () => {
247
- const res = await auth.api.signInSSO({
248
- body: {
249
- email: "test@localhost.com",
250
- callbackURL: "/dashboard",
234
+ it("should sign in with SSO provider with providerId", async () => {
235
+ const headers = new Headers();
236
+ const res = await authClient.signIn.sso({
237
+ providerId: "test",
238
+ callbackURL: "/dashboard",
239
+ fetchOptions: {
240
+ throw: true,
241
+ onSuccess: cookieSetter(headers),
251
242
  },
252
243
  });
253
244
  expect(res.url).toContain("http://localhost:8080/authorize");
254
245
  expect(res.url).toContain(
255
- "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Fdefault-test",
246
+ "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
256
247
  );
248
+
249
+ const { callbackURL } = await simulateOAuthFlow(res.url, headers);
250
+ expect(callbackURL).toContain("/dashboard");
257
251
  });
258
252
  });
259
253
 
260
254
  describe("SSO disable implicit sign in", async () => {
261
- const { auth, signInWithTestUser, customFetchImpl } =
262
- await getTestInstanceMemory({
255
+ const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
256
+ await getTestInstance({
263
257
  plugins: [sso({ disableImplicitSignUp: true }), organization()],
264
258
  });
265
259
 
260
+ const authClient = createAuthClient({
261
+ plugins: [ssoClient()],
262
+ baseURL: "http://localhost:3000",
263
+ fetchOptions: {
264
+ customFetchImpl,
265
+ },
266
+ });
267
+
266
268
  beforeAll(async () => {
267
269
  await server.issuer.keys.generate("RS256");
268
270
  server.issuer.on;
@@ -307,7 +309,7 @@ describe("SSO disable implicit sign in", async () => {
307
309
  });
308
310
 
309
311
  if (!location) throw new Error("No redirect location found");
310
-
312
+ const newHeaders = new Headers(headers);
311
313
  let callbackURL = "";
312
314
  await betterFetch(location, {
313
315
  method: "GET",
@@ -315,10 +317,11 @@ describe("SSO disable implicit sign in", async () => {
315
317
  headers,
316
318
  onError(context) {
317
319
  callbackURL = context.response.headers.get("location") || "";
320
+ cookieSetter(newHeaders)(context);
318
321
  },
319
322
  });
320
323
 
321
- return callbackURL;
324
+ return { callbackURL, headers: newHeaders };
322
325
  }
323
326
 
324
327
  it("should register a new SSO provider", async () => {
@@ -369,150 +372,61 @@ describe("SSO disable implicit sign in", async () => {
369
372
  userId: expect.any(String),
370
373
  });
371
374
  });
372
- it("should not allow creating a provider if limit is set to 0", async () => {
373
- const { auth, signInWithTestUser } = await getTestInstanceMemory({
374
- plugins: [sso({ providersLimit: 0 })],
375
- });
376
- const { headers } = await signInWithTestUser();
377
- await expect(
378
- auth.api.registerSSOProvider({
379
- body: {
380
- issuer: server.issuer.url!,
381
- domain: "localhost.com",
382
- oidcConfig: {
383
- clientId: "test",
384
- clientSecret: "test",
385
- },
386
- providerId: "test",
387
- },
388
- headers,
389
- }),
390
- ).rejects.toMatchObject({
391
- status: "FORBIDDEN",
392
- body: { message: "SSO provider registration is disabled" },
393
- });
394
- });
395
- it("should not allow creating a provider if limit is reached", async () => {
396
- const { auth, signInWithTestUser } = await getTestInstanceMemory({
397
- plugins: [sso({ providersLimit: 1 })],
398
- });
399
- const { headers } = await signInWithTestUser();
400
-
401
- await auth.api.registerSSOProvider({
402
- body: {
403
- issuer: server.issuer.url!,
404
- domain: "localhost.com",
405
- oidcConfig: {
406
- clientId: "test",
407
- clientSecret: "test",
408
- },
409
- providerId: "test-1",
410
- },
411
- headers,
412
- });
413
-
414
- await expect(
415
- auth.api.registerSSOProvider({
416
- body: {
417
- issuer: server.issuer.url!,
418
- domain: "localhost.com",
419
- oidcConfig: {
420
- clientId: "test",
421
- clientSecret: "test",
422
- },
423
- providerId: "test-2",
424
- },
425
- headers,
426
- }),
427
- ).rejects.toMatchObject({
428
- status: "FORBIDDEN",
429
- body: {
430
- message: "You have reached the maximum number of SSO providers",
431
- },
432
- });
433
- });
434
-
435
- it("should not allow creating a provider if limit from function is reached", async () => {
436
- const { auth, signInWithTestUser } = await getTestInstanceMemory({
437
- plugins: [sso({ providersLimit: async () => 1 })],
438
- });
439
- const { headers } = await signInWithTestUser();
440
-
441
- await auth.api.registerSSOProvider({
442
- body: {
443
- issuer: server.issuer.url!,
444
- domain: "localhost.com",
445
- oidcConfig: {
446
- clientId: "test",
447
- clientSecret: "test",
448
- },
449
- providerId: "test-1",
450
- },
451
- headers,
452
- });
453
375
 
454
- await expect(
455
- auth.api.registerSSOProvider({
456
- body: {
457
- issuer: server.issuer.url!,
458
- domain: "localhost.com",
459
- oidcConfig: {
460
- clientId: "test",
461
- clientSecret: "test",
462
- },
463
- providerId: "test-2",
464
- },
465
- headers,
466
- }),
467
- ).rejects.toMatchObject({
468
- status: "FORBIDDEN",
469
- body: {
470
- message: "You have reached the maximum number of SSO providers",
471
- },
472
- });
473
- });
474
376
  it("should not create user with SSO provider when sign ups are disabled", async () => {
475
- const res = await auth.api.signInSSO({
476
- body: {
477
- email: "my-email@localhost.com",
478
- callbackURL: "/dashboard",
377
+ const headers = new Headers();
378
+ const res = await authClient.signIn.sso({
379
+ email: "my-email@localhost.com",
380
+ callbackURL: "/dashboard",
381
+ fetchOptions: {
382
+ throw: true,
383
+ onSuccess: cookieSetter(headers),
479
384
  },
480
385
  });
481
386
  expect(res.url).toContain("http://localhost:8080/authorize");
482
387
  expect(res.url).toContain(
483
388
  "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
484
389
  );
485
- const headers = new Headers();
486
- const callbackURL = await simulateOAuthFlow(res.url, headers);
390
+ const { callbackURL } = await simulateOAuthFlow(res.url, headers);
487
391
  expect(callbackURL).toContain(
488
392
  "/api/auth/error/error?error=signup disabled",
489
393
  );
490
394
  });
491
395
 
492
396
  it("should create user with SSO provider when sign ups are disabled but sign up is requested", async () => {
493
- const res = await auth.api.signInSSO({
494
- body: {
495
- email: "my-email@localhost.com",
496
- callbackURL: "/dashboard",
497
- requestSignUp: true,
397
+ const headers = new Headers();
398
+ const res = await authClient.signIn.sso({
399
+ email: "my-email@localhost.com",
400
+ callbackURL: "/dashboard",
401
+ requestSignUp: true,
402
+ fetchOptions: {
403
+ throw: true,
404
+ onSuccess: cookieSetter(headers),
498
405
  },
499
406
  });
500
407
  expect(res.url).toContain("http://localhost:8080/authorize");
501
408
  expect(res.url).toContain(
502
409
  "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
503
410
  );
504
- const headers = new Headers();
505
- const callbackURL = await simulateOAuthFlow(res.url, headers);
411
+ const { callbackURL } = await simulateOAuthFlow(res.url, headers);
506
412
  expect(callbackURL).toContain("/dashboard");
507
413
  });
508
414
  });
509
415
 
510
416
  describe("provisioning", async (ctx) => {
511
- const { auth, signInWithTestUser, customFetchImpl } =
512
- await getTestInstanceMemory({
417
+ const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
418
+ await getTestInstance({
513
419
  plugins: [sso(), organization()],
514
420
  });
515
421
 
422
+ const authClient = createAuthClient({
423
+ plugins: [ssoClient()],
424
+ baseURL: "http://localhost:3000",
425
+ fetchOptions: {
426
+ customFetchImpl,
427
+ },
428
+ });
429
+
516
430
  beforeAll(async () => {
517
431
  await server.issuer.keys.generate("RS256");
518
432
  server.issuer.on;
@@ -540,12 +454,14 @@ describe("provisioning", async (ctx) => {
540
454
  if (!location) throw new Error("No redirect location found");
541
455
 
542
456
  let callbackURL = "";
457
+ const newHeaders = new Headers();
543
458
  await betterFetch(location, {
544
459
  method: "GET",
545
460
  customFetchImpl: fetchImpl || customFetchImpl,
546
461
  headers,
547
462
  onError(context) {
548
463
  callbackURL = context.response.headers.get("location") || "";
464
+ cookieSetter(newHeaders)(context);
549
465
  },
550
466
  });
551
467
 
@@ -605,18 +521,20 @@ describe("provisioning", async (ctx) => {
605
521
  expect(provider).toMatchObject({
606
522
  organizationId: organization?.id,
607
523
  });
608
-
609
- const res = await auth.api.signInSSO({
610
- body: {
611
- email: "my-email@localhost.com",
612
- callbackURL: "/dashboard",
524
+ const newHeaders = new Headers();
525
+ const res = await authClient.signIn.sso({
526
+ email: "my-email@localhost.com",
527
+ callbackURL: "/dashboard",
528
+ fetchOptions: {
529
+ onSuccess: cookieSetter(newHeaders),
530
+ throw: true,
613
531
  },
614
532
  });
615
533
  expect(res.url).toContain("http://localhost:8080/authorize");
616
534
  expect(res.url).toContain(
617
535
  "redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
618
536
  );
619
- const newHeaders = new Headers();
537
+
620
538
  const callbackURL = await simulateOAuthFlow(res.url, newHeaders);
621
539
  expect(callbackURL).toContain("/dashboard");
622
540
  const org = await auth.api.getFullOrganization({
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
  });
package/tsconfig.json CHANGED
@@ -5,10 +5,5 @@
5
5
  "outDir": "./dist",
6
6
  "lib": ["esnext", "dom", "dom.iterable"]
7
7
  },
8
- "references": [
9
- {
10
- "path": "../better-auth/tsconfig.json"
11
- }
12
- ],
13
8
  "include": ["src"]
14
9
  }