@better-auth/sso 1.5.0-beta.13 → 1.5.0-beta.15
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 +11 -11
- package/dist/client.d.mts +3 -2
- package/dist/client.mjs +1 -1
- package/dist/client.mjs.map +1 -1
- package/dist/{index-DCUy0gtM.d.mts → index-CbKvQr9M.d.mts} +129 -65
- package/dist/index.d.mts +56 -2
- package/dist/index.mjs +635 -236
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/client.ts +1 -1
- package/src/constants.ts +21 -0
- package/src/domain-verification.test.ts +46 -5
- package/src/index.ts +43 -2
- package/src/oidc/discovery.test.ts +7 -12
- package/src/oidc.test.ts +302 -1
- package/src/providers.test.ts +39 -45
- package/src/routes/domain-verification.ts +34 -12
- package/src/routes/helpers.ts +126 -0
- package/src/routes/providers.ts +16 -14
- package/src/routes/sso.ts +930 -359
- package/src/saml/algorithms.test.ts +1 -9
- package/src/saml/error-codes.ts +11 -0
- package/src/saml.test.ts +736 -4
- package/src/types.ts +53 -2
- package/src/utils.test.ts +3 -0
- package/vitest.config.ts +6 -1
package/src/providers.test.ts
CHANGED
|
@@ -459,11 +459,11 @@ describe("SSO provider read endpoints", () => {
|
|
|
459
459
|
});
|
|
460
460
|
});
|
|
461
461
|
|
|
462
|
-
describe("GET /sso/
|
|
462
|
+
describe("GET /sso/get-provider", () => {
|
|
463
463
|
it("should return 401 when not authenticated", async () => {
|
|
464
464
|
const { auth } = createTestAuth();
|
|
465
465
|
const response = await auth.api.getSSOProvider({
|
|
466
|
-
|
|
466
|
+
query: { providerId: "test" },
|
|
467
467
|
asResponse: true,
|
|
468
468
|
});
|
|
469
469
|
expect(response.status).toBe(401);
|
|
@@ -478,7 +478,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
478
478
|
});
|
|
479
479
|
|
|
480
480
|
const response = await auth.api.getSSOProvider({
|
|
481
|
-
|
|
481
|
+
query: { providerId: "nonexistent" },
|
|
482
482
|
headers,
|
|
483
483
|
asResponse: true,
|
|
484
484
|
});
|
|
@@ -504,7 +504,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
504
504
|
});
|
|
505
505
|
|
|
506
506
|
const response = await auth.api.getSSOProvider({
|
|
507
|
-
|
|
507
|
+
query: { providerId: "other-provider" },
|
|
508
508
|
headers: otherHeaders,
|
|
509
509
|
asResponse: true,
|
|
510
510
|
});
|
|
@@ -524,7 +524,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
524
524
|
await registerSAMLProvider(headers, "my-saml-provider");
|
|
525
525
|
|
|
526
526
|
const response = await auth.api.getSSOProvider({
|
|
527
|
-
|
|
527
|
+
query: { providerId: "my-saml-provider" },
|
|
528
528
|
headers,
|
|
529
529
|
});
|
|
530
530
|
|
|
@@ -559,7 +559,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
559
559
|
);
|
|
560
560
|
|
|
561
561
|
const response = await auth.api.getSSOProvider({
|
|
562
|
-
|
|
562
|
+
query: { providerId: "my-oidc-provider" },
|
|
563
563
|
headers,
|
|
564
564
|
});
|
|
565
565
|
|
|
@@ -589,7 +589,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
589
589
|
createOIDCProviderData(user!.id, "my-oidc-provider", "client123");
|
|
590
590
|
|
|
591
591
|
const response = await auth.api.getSSOProvider({
|
|
592
|
-
|
|
592
|
+
query: { providerId: "my-oidc-provider" },
|
|
593
593
|
headers,
|
|
594
594
|
});
|
|
595
595
|
|
|
@@ -629,7 +629,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
629
629
|
});
|
|
630
630
|
|
|
631
631
|
const response = await auth.api.getSSOProvider({
|
|
632
|
-
|
|
632
|
+
query: { providerId: "my-provider" },
|
|
633
633
|
headers,
|
|
634
634
|
});
|
|
635
635
|
|
|
@@ -671,7 +671,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
671
671
|
|
|
672
672
|
// Owner should be able to access it since they created the org (are admin)
|
|
673
673
|
const ownerResponse = await auth.api.getSSOProvider({
|
|
674
|
-
|
|
674
|
+
query: { providerId: "user-owned-org-provider" },
|
|
675
675
|
headers: ownerHeaders,
|
|
676
676
|
});
|
|
677
677
|
|
|
@@ -685,7 +685,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
685
685
|
});
|
|
686
686
|
|
|
687
687
|
const nonAdminResponse = await auth.api.getSSOProvider({
|
|
688
|
-
|
|
688
|
+
query: { providerId: "user-owned-org-provider" },
|
|
689
689
|
headers: nonAdminHeaders,
|
|
690
690
|
asResponse: true,
|
|
691
691
|
});
|
|
@@ -709,7 +709,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
709
709
|
await registerSAMLProvider(headers, "my-saml-provider");
|
|
710
710
|
|
|
711
711
|
const response = await auth.api.getSSOProvider({
|
|
712
|
-
|
|
712
|
+
query: { providerId: "my-saml-provider" },
|
|
713
713
|
headers,
|
|
714
714
|
});
|
|
715
715
|
|
|
@@ -745,7 +745,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
745
745
|
});
|
|
746
746
|
|
|
747
747
|
const response = await auth.api.getSSOProvider({
|
|
748
|
-
|
|
748
|
+
query: { providerId: "my-saml-provider" },
|
|
749
749
|
headers,
|
|
750
750
|
});
|
|
751
751
|
|
|
@@ -771,7 +771,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
771
771
|
createOIDCProviderData(user!.id, "my-oidc-provider", "abc");
|
|
772
772
|
|
|
773
773
|
const response = await auth.api.getSSOProvider({
|
|
774
|
-
|
|
774
|
+
query: { providerId: "my-oidc-provider" },
|
|
775
775
|
headers,
|
|
776
776
|
});
|
|
777
777
|
|
|
@@ -779,12 +779,11 @@ describe("SSO provider read endpoints", () => {
|
|
|
779
779
|
});
|
|
780
780
|
});
|
|
781
781
|
|
|
782
|
-
describe("
|
|
782
|
+
describe("POST /sso/update-provider", () => {
|
|
783
783
|
it("should return 401 when not authenticated", async () => {
|
|
784
784
|
const { auth } = createTestAuth();
|
|
785
785
|
const response = await auth.api.updateSSOProvider({
|
|
786
|
-
|
|
787
|
-
body: { domain: "new-domain.com" },
|
|
786
|
+
body: { providerId: "test", domain: "new-domain.com" },
|
|
788
787
|
asResponse: true,
|
|
789
788
|
});
|
|
790
789
|
expect(response.status).toBe(401);
|
|
@@ -799,8 +798,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
799
798
|
});
|
|
800
799
|
|
|
801
800
|
const response = await auth.api.updateSSOProvider({
|
|
802
|
-
|
|
803
|
-
body: { domain: "new-domain.com" },
|
|
801
|
+
body: { providerId: "nonexistent", domain: "new-domain.com" },
|
|
804
802
|
headers,
|
|
805
803
|
asResponse: true,
|
|
806
804
|
});
|
|
@@ -826,8 +824,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
826
824
|
});
|
|
827
825
|
|
|
828
826
|
const response = await auth.api.updateSSOProvider({
|
|
829
|
-
|
|
830
|
-
body: { domain: "new-domain.com" },
|
|
827
|
+
body: { providerId: "other-provider", domain: "new-domain.com" },
|
|
831
828
|
headers: otherHeaders,
|
|
832
829
|
asResponse: true,
|
|
833
830
|
});
|
|
@@ -849,8 +846,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
849
846
|
await registerSAMLProvider(headers, "my-saml-provider");
|
|
850
847
|
|
|
851
848
|
const updated = await auth.api.updateSSOProvider({
|
|
852
|
-
|
|
853
|
-
body: { domain: "new-domain.com" },
|
|
849
|
+
body: { providerId: "my-saml-provider", domain: "new-domain.com" },
|
|
854
850
|
headers,
|
|
855
851
|
});
|
|
856
852
|
|
|
@@ -871,8 +867,8 @@ describe("SSO provider read endpoints", () => {
|
|
|
871
867
|
await registerSAMLProvider(headers, "my-saml-provider");
|
|
872
868
|
|
|
873
869
|
const updated = await auth.api.updateSSOProvider({
|
|
874
|
-
params: { providerId: "my-saml-provider" },
|
|
875
870
|
body: {
|
|
871
|
+
providerId: "my-saml-provider",
|
|
876
872
|
samlConfig: {
|
|
877
873
|
audience: "new-audience",
|
|
878
874
|
wantAssertionsSigned: false,
|
|
@@ -904,8 +900,8 @@ describe("SSO provider read endpoints", () => {
|
|
|
904
900
|
createOIDCProviderData(user!.id, "my-oidc-provider", "client123");
|
|
905
901
|
|
|
906
902
|
const updated = await auth.api.updateSSOProvider({
|
|
907
|
-
params: { providerId: "my-oidc-provider" },
|
|
908
903
|
body: {
|
|
904
|
+
providerId: "my-oidc-provider",
|
|
909
905
|
oidcConfig: {
|
|
910
906
|
scopes: ["openid", "email", "profile", "custom"],
|
|
911
907
|
pkce: false,
|
|
@@ -936,8 +932,10 @@ describe("SSO provider read endpoints", () => {
|
|
|
936
932
|
await registerSAMLProvider(headers, "my-saml-provider");
|
|
937
933
|
|
|
938
934
|
const updated = await auth.api.updateSSOProvider({
|
|
939
|
-
|
|
940
|
-
|
|
935
|
+
body: {
|
|
936
|
+
providerId: "my-saml-provider",
|
|
937
|
+
issuer: "https://new-issuer.example.com",
|
|
938
|
+
},
|
|
941
939
|
headers,
|
|
942
940
|
});
|
|
943
941
|
|
|
@@ -957,8 +955,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
957
955
|
await registerSAMLProvider(headers, "my-saml-provider");
|
|
958
956
|
|
|
959
957
|
const response = await auth.api.updateSSOProvider({
|
|
960
|
-
|
|
961
|
-
body: { issuer: "invalid-url" },
|
|
958
|
+
body: { providerId: "my-saml-provider", issuer: "invalid-url" },
|
|
962
959
|
headers,
|
|
963
960
|
asResponse: true,
|
|
964
961
|
});
|
|
@@ -979,8 +976,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
979
976
|
await registerSAMLProvider(headers, "my-saml-provider");
|
|
980
977
|
|
|
981
978
|
const response = await auth.api.updateSSOProvider({
|
|
982
|
-
|
|
983
|
-
body: {},
|
|
979
|
+
body: { providerId: "my-saml-provider" },
|
|
984
980
|
headers,
|
|
985
981
|
asResponse: true,
|
|
986
982
|
});
|
|
@@ -1020,8 +1016,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
1020
1016
|
await addMember(adminUser!.id, org!.id, "admin", ownerHeaders);
|
|
1021
1017
|
|
|
1022
1018
|
const updated = await auth.api.updateSSOProvider({
|
|
1023
|
-
|
|
1024
|
-
body: { domain: "new-domain.com" },
|
|
1019
|
+
body: { providerId: "org-provider", domain: "new-domain.com" },
|
|
1025
1020
|
headers: adminHeaders,
|
|
1026
1021
|
});
|
|
1027
1022
|
|
|
@@ -1060,8 +1055,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
1060
1055
|
await addMember(memberUser!.id, org!.id, "member", ownerHeaders);
|
|
1061
1056
|
|
|
1062
1057
|
const response = await auth.api.updateSSOProvider({
|
|
1063
|
-
|
|
1064
|
-
body: { domain: "new-domain.com" },
|
|
1058
|
+
body: { providerId: "org-provider", domain: "new-domain.com" },
|
|
1065
1059
|
headers: memberHeaders,
|
|
1066
1060
|
asResponse: true,
|
|
1067
1061
|
});
|
|
@@ -1085,8 +1079,8 @@ describe("SSO provider read endpoints", () => {
|
|
|
1085
1079
|
createOIDCProviderData(user!.id, "my-oidc-provider", "client123");
|
|
1086
1080
|
|
|
1087
1081
|
const response = await auth.api.updateSSOProvider({
|
|
1088
|
-
params: { providerId: "my-oidc-provider" },
|
|
1089
1082
|
body: {
|
|
1083
|
+
providerId: "my-oidc-provider",
|
|
1090
1084
|
samlConfig: {
|
|
1091
1085
|
entryPoint: "https://idp.example.com/sso",
|
|
1092
1086
|
cert: TEST_CERT,
|
|
@@ -1114,8 +1108,8 @@ describe("SSO provider read endpoints", () => {
|
|
|
1114
1108
|
await registerSAMLProvider(headers, "my-saml-provider");
|
|
1115
1109
|
|
|
1116
1110
|
const response = await auth.api.updateSSOProvider({
|
|
1117
|
-
params: { providerId: "my-saml-provider" },
|
|
1118
1111
|
body: {
|
|
1112
|
+
providerId: "my-saml-provider",
|
|
1119
1113
|
oidcConfig: {
|
|
1120
1114
|
clientId: "new-client-id",
|
|
1121
1115
|
clientSecret: "new-secret",
|
|
@@ -1129,11 +1123,11 @@ describe("SSO provider read endpoints", () => {
|
|
|
1129
1123
|
});
|
|
1130
1124
|
});
|
|
1131
1125
|
|
|
1132
|
-
describe("
|
|
1126
|
+
describe("POST /sso/delete-provider", () => {
|
|
1133
1127
|
it("should return 401 when not authenticated", async () => {
|
|
1134
1128
|
const { auth } = createTestAuth();
|
|
1135
1129
|
const response = await auth.api.deleteSSOProvider({
|
|
1136
|
-
|
|
1130
|
+
body: { providerId: "test" },
|
|
1137
1131
|
asResponse: true,
|
|
1138
1132
|
});
|
|
1139
1133
|
expect(response.status).toBe(401);
|
|
@@ -1148,7 +1142,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
1148
1142
|
});
|
|
1149
1143
|
|
|
1150
1144
|
const response = await auth.api.deleteSSOProvider({
|
|
1151
|
-
|
|
1145
|
+
body: { providerId: "nonexistent" },
|
|
1152
1146
|
headers,
|
|
1153
1147
|
asResponse: true,
|
|
1154
1148
|
});
|
|
@@ -1174,7 +1168,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
1174
1168
|
});
|
|
1175
1169
|
|
|
1176
1170
|
const response = await auth.api.deleteSSOProvider({
|
|
1177
|
-
|
|
1171
|
+
body: { providerId: "other-provider" },
|
|
1178
1172
|
headers: otherHeaders,
|
|
1179
1173
|
asResponse: true,
|
|
1180
1174
|
});
|
|
@@ -1194,14 +1188,14 @@ describe("SSO provider read endpoints", () => {
|
|
|
1194
1188
|
await registerSAMLProvider(headers, "my-saml-provider");
|
|
1195
1189
|
|
|
1196
1190
|
const deleteResponse = await auth.api.deleteSSOProvider({
|
|
1197
|
-
|
|
1191
|
+
body: { providerId: "my-saml-provider" },
|
|
1198
1192
|
headers,
|
|
1199
1193
|
});
|
|
1200
1194
|
|
|
1201
1195
|
expect(deleteResponse.success).toBe(true);
|
|
1202
1196
|
|
|
1203
1197
|
const getResponse = await auth.api.getSSOProvider({
|
|
1204
|
-
|
|
1198
|
+
query: { providerId: "my-saml-provider" },
|
|
1205
1199
|
headers,
|
|
1206
1200
|
asResponse: true,
|
|
1207
1201
|
});
|
|
@@ -1241,7 +1235,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
1241
1235
|
await addMember(adminUser!.id, org!.id, "admin", ownerHeaders);
|
|
1242
1236
|
|
|
1243
1237
|
const deleteResponse = await auth.api.deleteSSOProvider({
|
|
1244
|
-
|
|
1238
|
+
body: { providerId: "org-provider" },
|
|
1245
1239
|
headers: adminHeaders,
|
|
1246
1240
|
});
|
|
1247
1241
|
|
|
@@ -1280,7 +1274,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
1280
1274
|
await addMember(memberUser!.id, org!.id, "member", ownerHeaders);
|
|
1281
1275
|
|
|
1282
1276
|
const response = await auth.api.deleteSSOProvider({
|
|
1283
|
-
|
|
1277
|
+
body: { providerId: "org-provider" },
|
|
1284
1278
|
headers: memberHeaders,
|
|
1285
1279
|
asResponse: true,
|
|
1286
1280
|
});
|
|
@@ -1316,7 +1310,7 @@ describe("SSO provider read endpoints", () => {
|
|
|
1316
1310
|
const accountCountBefore = data.account.length;
|
|
1317
1311
|
|
|
1318
1312
|
await auth.api.deleteSSOProvider({
|
|
1319
|
-
|
|
1313
|
+
body: { providerId: "my-saml-provider" },
|
|
1320
1314
|
headers,
|
|
1321
1315
|
});
|
|
1322
1316
|
|
|
@@ -8,10 +8,22 @@ import { generateRandomString } from "better-auth/crypto";
|
|
|
8
8
|
import * as z from "zod/v4";
|
|
9
9
|
import type { SSOOptions, SSOProvider } from "../types";
|
|
10
10
|
|
|
11
|
+
const DNS_LABEL_MAX_LENGTH = 63;
|
|
12
|
+
const DEFAULT_TOKEN_PREFIX = "better-auth-token";
|
|
13
|
+
|
|
11
14
|
const domainVerificationBodySchema = z.object({
|
|
12
15
|
providerId: z.string(),
|
|
13
16
|
});
|
|
14
17
|
|
|
18
|
+
export function getVerificationIdentifier(
|
|
19
|
+
options: SSOOptions,
|
|
20
|
+
providerId: string,
|
|
21
|
+
): string {
|
|
22
|
+
const tokenPrefix =
|
|
23
|
+
options.domainVerification?.tokenPrefix || DEFAULT_TOKEN_PREFIX;
|
|
24
|
+
return `_${tokenPrefix}-${providerId}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
export const requestDomainVerification = (options: SSOOptions) => {
|
|
16
28
|
return createAuthEndpoint(
|
|
17
29
|
"/sso/request-domain-verification",
|
|
@@ -83,15 +95,18 @@ export const requestDomainVerification = (options: SSOOptions) => {
|
|
|
83
95
|
});
|
|
84
96
|
}
|
|
85
97
|
|
|
98
|
+
const identifier = getVerificationIdentifier(
|
|
99
|
+
options,
|
|
100
|
+
provider.providerId,
|
|
101
|
+
);
|
|
102
|
+
|
|
86
103
|
const activeVerification =
|
|
87
104
|
await ctx.context.adapter.findOne<Verification>({
|
|
88
105
|
model: "verification",
|
|
89
106
|
where: [
|
|
90
107
|
{
|
|
91
108
|
field: "identifier",
|
|
92
|
-
value:
|
|
93
|
-
? `${options.domainVerification?.tokenPrefix}-${provider.providerId}`
|
|
94
|
-
: `better-auth-token-${provider.providerId}`,
|
|
109
|
+
value: identifier,
|
|
95
110
|
},
|
|
96
111
|
{ field: "expiresAt", value: new Date(), operator: "gt" },
|
|
97
112
|
],
|
|
@@ -106,9 +121,7 @@ export const requestDomainVerification = (options: SSOOptions) => {
|
|
|
106
121
|
await ctx.context.adapter.create<Verification>({
|
|
107
122
|
model: "verification",
|
|
108
123
|
data: {
|
|
109
|
-
identifier
|
|
110
|
-
? `${options.domainVerification?.tokenPrefix}-${provider.providerId}`
|
|
111
|
-
: `better-auth-token-${provider.providerId}`,
|
|
124
|
+
identifier,
|
|
112
125
|
createdAt: new Date(),
|
|
113
126
|
updatedAt: new Date(),
|
|
114
127
|
value: domainVerificationToken,
|
|
@@ -199,15 +212,25 @@ export const verifyDomain = (options: SSOOptions) => {
|
|
|
199
212
|
});
|
|
200
213
|
}
|
|
201
214
|
|
|
215
|
+
const identifier = getVerificationIdentifier(
|
|
216
|
+
options,
|
|
217
|
+
provider.providerId,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (identifier.length > DNS_LABEL_MAX_LENGTH) {
|
|
221
|
+
throw new APIError("BAD_REQUEST", {
|
|
222
|
+
message: `Verification identifier exceeds the DNS label limit of ${DNS_LABEL_MAX_LENGTH} characters`,
|
|
223
|
+
code: "IDENTIFIER_TOO_LONG",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
202
227
|
const activeVerification =
|
|
203
228
|
await ctx.context.adapter.findOne<Verification>({
|
|
204
229
|
model: "verification",
|
|
205
230
|
where: [
|
|
206
231
|
{
|
|
207
232
|
field: "identifier",
|
|
208
|
-
value:
|
|
209
|
-
? `${options.domainVerification?.tokenPrefix}-${provider.providerId}`
|
|
210
|
-
: `better-auth-token-${provider.providerId}`,
|
|
233
|
+
value: identifier,
|
|
211
234
|
},
|
|
212
235
|
{ field: "expiresAt", value: new Date(), operator: "gt" },
|
|
213
236
|
],
|
|
@@ -237,9 +260,8 @@ export const verifyDomain = (options: SSOOptions) => {
|
|
|
237
260
|
}
|
|
238
261
|
|
|
239
262
|
try {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
);
|
|
263
|
+
const hostname = new URL(provider.domain).hostname;
|
|
264
|
+
const dnsRecords = await dns.resolveTxt(`${identifier}.${hostname}`);
|
|
243
265
|
records = dnsRecords.flat();
|
|
244
266
|
} catch (error) {
|
|
245
267
|
ctx.context.logger.warn(
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { DBAdapter } from "@better-auth/core/db/adapter";
|
|
2
|
+
import * as saml from "samlify";
|
|
3
|
+
import type { SAMLConfig, SSOOptions, SSOProvider } from "../types";
|
|
4
|
+
import { safeJsonParse } from "../utils";
|
|
5
|
+
|
|
6
|
+
export async function findSAMLProvider(
|
|
7
|
+
providerId: string,
|
|
8
|
+
options: SSOOptions | undefined,
|
|
9
|
+
adapter: DBAdapter,
|
|
10
|
+
): Promise<SSOProvider<SSOOptions> | null> {
|
|
11
|
+
if (options?.defaultSSO?.length) {
|
|
12
|
+
const match = options.defaultSSO.find((p) => p.providerId === providerId);
|
|
13
|
+
if (match) {
|
|
14
|
+
return {
|
|
15
|
+
...match,
|
|
16
|
+
userId: "default",
|
|
17
|
+
issuer: match.samlConfig?.issuer || "",
|
|
18
|
+
...(options.domainVerification?.enabled
|
|
19
|
+
? { domainVerified: true }
|
|
20
|
+
: {}),
|
|
21
|
+
} as SSOProvider<SSOOptions>;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const res = await adapter.findOne<SSOProvider<SSOOptions>>({
|
|
26
|
+
model: "ssoProvider",
|
|
27
|
+
where: [{ field: "providerId", value: providerId }],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!res) return null;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
...res,
|
|
34
|
+
samlConfig: res.samlConfig
|
|
35
|
+
? safeJsonParse<SAMLConfig>(res.samlConfig as unknown as string) ||
|
|
36
|
+
undefined
|
|
37
|
+
: undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createSP(
|
|
42
|
+
config: SAMLConfig,
|
|
43
|
+
baseURL: string,
|
|
44
|
+
providerId: string,
|
|
45
|
+
sloOptions?: {
|
|
46
|
+
wantLogoutRequestSigned?: boolean;
|
|
47
|
+
wantLogoutResponseSigned?: boolean;
|
|
48
|
+
},
|
|
49
|
+
) {
|
|
50
|
+
const sloLocation = `${baseURL}/sso/saml2/sp/slo/${providerId}`;
|
|
51
|
+
return saml.ServiceProvider({
|
|
52
|
+
entityID: config.spMetadata?.entityID || config.issuer,
|
|
53
|
+
assertionConsumerService: [
|
|
54
|
+
{
|
|
55
|
+
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
56
|
+
Location:
|
|
57
|
+
config.callbackUrl || `${baseURL}/sso/saml2/sp/acs/${providerId}`,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
singleLogoutService: [
|
|
61
|
+
{
|
|
62
|
+
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
|
|
63
|
+
Location: sloLocation,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
|
67
|
+
Location: sloLocation,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
wantMessageSigned: config.wantAssertionsSigned || false,
|
|
71
|
+
wantLogoutRequestSigned: sloOptions?.wantLogoutRequestSigned ?? false,
|
|
72
|
+
wantLogoutResponseSigned: sloOptions?.wantLogoutResponseSigned ?? false,
|
|
73
|
+
metadata: config.spMetadata?.metadata,
|
|
74
|
+
privateKey: config.spMetadata?.privateKey || config.privateKey,
|
|
75
|
+
privateKeyPass: config.spMetadata?.privateKeyPass,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createIdP(config: SAMLConfig) {
|
|
80
|
+
const idpData = config.idpMetadata;
|
|
81
|
+
if (idpData?.metadata) {
|
|
82
|
+
return saml.IdentityProvider({
|
|
83
|
+
metadata: idpData.metadata,
|
|
84
|
+
privateKey: idpData.privateKey,
|
|
85
|
+
privateKeyPass: idpData.privateKeyPass,
|
|
86
|
+
encPrivateKey: idpData.encPrivateKey,
|
|
87
|
+
encPrivateKeyPass: idpData.encPrivateKeyPass,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return saml.IdentityProvider({
|
|
91
|
+
entityID: idpData?.entityID || config.issuer,
|
|
92
|
+
singleSignOnService: idpData?.singleSignOnService || [
|
|
93
|
+
{
|
|
94
|
+
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
|
|
95
|
+
Location: config.entryPoint,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
singleLogoutService: idpData?.singleLogoutService,
|
|
99
|
+
signingCert: idpData?.cert || config.cert,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function escapeHtml(str: string | undefined | null): string {
|
|
104
|
+
if (!str) return "";
|
|
105
|
+
return String(str)
|
|
106
|
+
.replace(/&/g, "&")
|
|
107
|
+
.replace(/</g, "<")
|
|
108
|
+
.replace(/>/g, ">")
|
|
109
|
+
.replace(/"/g, """)
|
|
110
|
+
.replace(/'/g, "'");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createSAMLPostForm(
|
|
114
|
+
action: string,
|
|
115
|
+
samlParam: string,
|
|
116
|
+
samlValue: string,
|
|
117
|
+
relayState?: string,
|
|
118
|
+
): Response {
|
|
119
|
+
const safeAction = escapeHtml(action);
|
|
120
|
+
const safeSamlParam = escapeHtml(samlParam);
|
|
121
|
+
const safeSamlValue = escapeHtml(samlValue);
|
|
122
|
+
const safeRelayState = relayState ? escapeHtml(relayState) : undefined;
|
|
123
|
+
|
|
124
|
+
const html = `<!DOCTYPE html><html><body onload="document.forms[0].submit();"><form method="POST" action="${safeAction}"><input type="hidden" name="${safeSamlParam}" value="${safeSamlValue}" />${safeRelayState ? `<input type="hidden" name="RelayState" value="${safeRelayState}" />` : ""}<noscript><input type="submit" value="Continue" /></noscript></form></body></html>`;
|
|
125
|
+
return new Response(html, { headers: { "Content-Type": "text/html" } });
|
|
126
|
+
}
|
package/src/routes/providers.ts
CHANGED
|
@@ -233,7 +233,7 @@ export const listSSOProviders = () => {
|
|
|
233
233
|
);
|
|
234
234
|
};
|
|
235
235
|
|
|
236
|
-
const
|
|
236
|
+
const getSSOProviderQuerySchema = z.object({
|
|
237
237
|
providerId: z.string(),
|
|
238
238
|
});
|
|
239
239
|
|
|
@@ -280,11 +280,11 @@ async function checkProviderAccess(
|
|
|
280
280
|
|
|
281
281
|
export const getSSOProvider = () => {
|
|
282
282
|
return createAuthEndpoint(
|
|
283
|
-
"/sso/
|
|
283
|
+
"/sso/get-provider",
|
|
284
284
|
{
|
|
285
285
|
method: "GET",
|
|
286
286
|
use: [sessionMiddleware],
|
|
287
|
-
|
|
287
|
+
query: getSSOProviderQuerySchema,
|
|
288
288
|
metadata: {
|
|
289
289
|
openapi: {
|
|
290
290
|
operationId: "getSSOProvider",
|
|
@@ -305,7 +305,7 @@ export const getSSOProvider = () => {
|
|
|
305
305
|
},
|
|
306
306
|
},
|
|
307
307
|
async (ctx) => {
|
|
308
|
-
const { providerId } = ctx.
|
|
308
|
+
const { providerId } = ctx.query;
|
|
309
309
|
|
|
310
310
|
const provider = await checkProviderAccess(ctx, providerId);
|
|
311
311
|
|
|
@@ -387,12 +387,13 @@ function mergeOIDCConfig(
|
|
|
387
387
|
|
|
388
388
|
export const updateSSOProvider = (options: SSOOptions) => {
|
|
389
389
|
return createAuthEndpoint(
|
|
390
|
-
"/sso/
|
|
390
|
+
"/sso/update-provider",
|
|
391
391
|
{
|
|
392
|
-
method: "
|
|
392
|
+
method: "POST",
|
|
393
393
|
use: [sessionMiddleware],
|
|
394
|
-
|
|
395
|
-
|
|
394
|
+
body: updateSSOProviderBodySchema.extend({
|
|
395
|
+
providerId: z.string(),
|
|
396
|
+
}),
|
|
396
397
|
metadata: {
|
|
397
398
|
openapi: {
|
|
398
399
|
operationId: "updateSSOProvider",
|
|
@@ -414,8 +415,7 @@ export const updateSSOProvider = (options: SSOOptions) => {
|
|
|
414
415
|
},
|
|
415
416
|
},
|
|
416
417
|
async (ctx) => {
|
|
417
|
-
const { providerId } = ctx.
|
|
418
|
-
const body = ctx.body;
|
|
418
|
+
const { providerId, ...body } = ctx.body;
|
|
419
419
|
|
|
420
420
|
const { issuer, domain, samlConfig, oidcConfig } = body;
|
|
421
421
|
if (!issuer && !domain && !samlConfig && !oidcConfig) {
|
|
@@ -525,11 +525,13 @@ export const updateSSOProvider = (options: SSOOptions) => {
|
|
|
525
525
|
|
|
526
526
|
export const deleteSSOProvider = () => {
|
|
527
527
|
return createAuthEndpoint(
|
|
528
|
-
"/sso/
|
|
528
|
+
"/sso/delete-provider",
|
|
529
529
|
{
|
|
530
|
-
method: "
|
|
530
|
+
method: "POST",
|
|
531
531
|
use: [sessionMiddleware],
|
|
532
|
-
|
|
532
|
+
body: z.object({
|
|
533
|
+
providerId: z.string(),
|
|
534
|
+
}),
|
|
533
535
|
metadata: {
|
|
534
536
|
openapi: {
|
|
535
537
|
operationId: "deleteSSOProvider",
|
|
@@ -550,7 +552,7 @@ export const deleteSSOProvider = () => {
|
|
|
550
552
|
},
|
|
551
553
|
},
|
|
552
554
|
async (ctx) => {
|
|
553
|
-
const { providerId } = ctx.
|
|
555
|
+
const { providerId } = ctx.body;
|
|
554
556
|
|
|
555
557
|
await checkProviderAccess(ctx, providerId);
|
|
556
558
|
|