@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.
- 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 +37 -0
- package/src/saml.test.ts +49 -0
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
|
@@ -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
|
});
|