@better-auth/sso 1.4.0-beta.5 → 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.
- package/.turbo/turbo-build.log +4 -4
- package/dist/index.cjs +74 -14
- package/dist/index.mjs +74 -14
- package/package.json +4 -4
- package/src/index.ts +97 -20
- package/src/oidc.test.ts +128 -210
- package/src/saml.test.ts +49 -0
- package/tsconfig.json +0 -5
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
|
|
2
|
-
> @better-auth/sso@1.4.0-beta.
|
|
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:
|
|
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:
|
|
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):
|
|
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 =
|
|
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 ?
|
|
671
|
-
|
|
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 :
|
|
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:
|
|
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 ?
|
|
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 =
|
|
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
|
|
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
|
|
1143
|
+
signingCert: idpData?.cert || parsedSamlConfig.cert,
|
|
1086
1144
|
wantAuthnRequestsSigned: parsedSamlConfig.wantAssertionsSigned || false,
|
|
1087
|
-
isAssertionEncrypted: idpData
|
|
1088
|
-
encPrivateKey: idpData
|
|
1089
|
-
encPrivateKeyPass: idpData
|
|
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 ?
|
|
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 =
|
|
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 ?
|
|
654
|
-
|
|
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 :
|
|
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:
|
|
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 ?
|
|
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 =
|
|
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
|
|
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
|
|
1126
|
+
signingCert: idpData?.cert || parsedSamlConfig.cert,
|
|
1069
1127
|
wantAuthnRequestsSigned: parsedSamlConfig.wantAssertionsSigned || false,
|
|
1070
|
-
isAssertionEncrypted: idpData
|
|
1071
|
-
encPrivateKey: idpData
|
|
1072
|
-
encPrivateKeyPass: idpData
|
|
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 ?
|
|
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.
|
|
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": "
|
|
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.
|
|
61
|
+
"better-auth": "^1.4.0-beta.7"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"better-auth": "1.4.0-beta.
|
|
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
|
|
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
|
-
?
|
|
1098
|
+
? safeJsonParse<OIDCConfig>(
|
|
1099
|
+
res.oidcConfig as unknown as string,
|
|
1100
|
+
) || undefined
|
|
1044
1101
|
: undefined,
|
|
1045
1102
|
samlConfig: res.samlConfig
|
|
1046
|
-
?
|
|
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
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
|
1157
|
+
const parsedSamlConfig =
|
|
1098
1158
|
typeof provider.samlConfig === "object"
|
|
1099
1159
|
? provider.samlConfig
|
|
1100
|
-
:
|
|
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:
|
|
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
|
-
?
|
|
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 =
|
|
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
|
|
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
|
|
1638
|
+
signingCert: idpData?.cert || parsedSamlConfig.cert,
|
|
1564
1639
|
wantAuthnRequestsSigned:
|
|
1565
1640
|
parsedSamlConfig.wantAssertionsSigned || false,
|
|
1566
|
-
isAssertionEncrypted: idpData
|
|
1567
|
-
encPrivateKey: idpData
|
|
1568
|
-
encPrivateKeyPass: idpData
|
|
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
|
-
?
|
|
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
|
@@ -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
|
|
6
|
-
import {
|
|
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
|
|
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
|
|
150
|
-
const
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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%
|
|
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
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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%
|
|
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
|
|
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
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
|
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
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
|
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
|
|
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
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
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
|
});
|