@better-auth/sso 1.3.18 → 1.4.0-beta.1

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/dist/index.cjs CHANGED
@@ -76,18 +76,8 @@ const sso = (options) => {
76
76
  });
77
77
  }
78
78
  const parsedSamlConfig = JSON.parse(provider.samlConfig);
79
- const sp = parsedSamlConfig.spMetadata.metadata ? saml__namespace.ServiceProvider({
79
+ const sp = saml__namespace.ServiceProvider({
80
80
  metadata: parsedSamlConfig.spMetadata.metadata
81
- }) : saml__namespace.SPMetadata({
82
- entityID: parsedSamlConfig.spMetadata?.entityID || parsedSamlConfig.issuer,
83
- assertionConsumerService: [
84
- {
85
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
86
- Location: parsedSamlConfig.callbackUrl || `${ctx.context.baseURL}/sso/saml2/sp/acs/${provider.id}`
87
- }
88
- ],
89
- wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
90
- nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
91
81
  });
92
82
  return new Response(sp.getMetadata(), {
93
83
  headers: {
@@ -136,25 +126,7 @@ const sso = (options) => {
136
126
  }).optional(),
137
127
  pkce: z__namespace.boolean({}).meta({
138
128
  description: "Whether to use PKCE for the authorization flow"
139
- }).default(true).optional(),
140
- mapping: z__namespace.object({
141
- id: z__namespace.string({}).meta({
142
- description: "Field mapping for user ID (defaults to 'sub')"
143
- }),
144
- email: z__namespace.string({}).meta({
145
- description: "Field mapping for email (defaults to 'email')"
146
- }),
147
- emailVerified: z__namespace.string({}).meta({
148
- description: "Field mapping for email verification (defaults to 'email_verified')"
149
- }).optional(),
150
- name: z__namespace.string({}).meta({
151
- description: "Field mapping for name (defaults to 'name')"
152
- }),
153
- image: z__namespace.string({}).meta({
154
- description: "Field mapping for image (defaults to 'picture')"
155
- }).optional(),
156
- extraFields: z__namespace.record(z__namespace.string(), z__namespace.any()).optional()
157
- }).optional()
129
+ }).default(true).optional()
158
130
  }).optional(),
159
131
  samlConfig: z__namespace.object({
160
132
  entryPoint: z__namespace.string({}).meta({
@@ -168,30 +140,15 @@ const sso = (options) => {
168
140
  }),
169
141
  audience: z__namespace.string().optional(),
170
142
  idpMetadata: z__namespace.object({
171
- metadata: z__namespace.string().optional(),
172
- entityID: z__namespace.string().optional(),
173
- cert: z__namespace.string().optional(),
143
+ metadata: z__namespace.string(),
174
144
  privateKey: z__namespace.string().optional(),
175
145
  privateKeyPass: z__namespace.string().optional(),
176
146
  isAssertionEncrypted: z__namespace.boolean().optional(),
177
147
  encPrivateKey: z__namespace.string().optional(),
178
- encPrivateKeyPass: z__namespace.string().optional(),
179
- singleSignOnService: z__namespace.array(
180
- z__namespace.object({
181
- Binding: z__namespace.string().meta({
182
- description: "The binding type for the SSO service"
183
- }),
184
- Location: z__namespace.string().meta({
185
- description: "The URL for the SSO service"
186
- })
187
- })
188
- ).optional().meta({
189
- description: "Single Sign-On service configuration"
190
- })
148
+ encPrivateKeyPass: z__namespace.string().optional()
191
149
  }).optional(),
192
150
  spMetadata: z__namespace.object({
193
- metadata: z__namespace.string().optional(),
194
- entityID: z__namespace.string().optional(),
151
+ metadata: z__namespace.string(),
195
152
  binding: z__namespace.string().optional(),
196
153
  privateKey: z__namespace.string().optional(),
197
154
  privateKeyPass: z__namespace.string().optional(),
@@ -205,28 +162,25 @@ const sso = (options) => {
205
162
  identifierFormat: z__namespace.string().optional(),
206
163
  privateKey: z__namespace.string().optional(),
207
164
  decryptionPvk: z__namespace.string().optional(),
208
- additionalParams: z__namespace.record(z__namespace.string(), z__namespace.any()).optional(),
209
- mapping: z__namespace.object({
210
- id: z__namespace.string({}).meta({
211
- description: "Field mapping for user ID (defaults to 'nameID')"
212
- }),
213
- email: z__namespace.string({}).meta({
214
- description: "Field mapping for email (defaults to 'email')"
215
- }),
216
- emailVerified: z__namespace.string({}).meta({
217
- description: "Field mapping for email verification"
218
- }).optional(),
219
- name: z__namespace.string({}).meta({
220
- description: "Field mapping for name (defaults to 'displayName')"
221
- }),
222
- firstName: z__namespace.string({}).meta({
223
- description: "Field mapping for first name (defaults to 'givenName')"
224
- }).optional(),
225
- lastName: z__namespace.string({}).meta({
226
- description: "Field mapping for last name (defaults to 'surname')"
227
- }).optional(),
228
- extraFields: z__namespace.record(z__namespace.string(), z__namespace.any()).optional()
229
- }).optional()
165
+ additionalParams: z__namespace.record(z__namespace.string(), z__namespace.any()).optional()
166
+ }).optional(),
167
+ mapping: z__namespace.object({
168
+ id: z__namespace.string({}).meta({
169
+ description: "The field in the user info response that contains the id. Defaults to 'sub'"
170
+ }),
171
+ email: z__namespace.string({}).meta({
172
+ description: "The field in the user info response that contains the email. Defaults to 'email'"
173
+ }),
174
+ emailVerified: z__namespace.string({}).meta({
175
+ description: "The field in the user info response that contains whether the email is verified. defaults to 'email_verified'"
176
+ }).optional(),
177
+ name: z__namespace.string({}).meta({
178
+ description: "The field in the user info response that contains the name. Defaults to 'name'"
179
+ }),
180
+ image: z__namespace.string({}).meta({
181
+ description: "The field in the user info response that contains the image. Defaults to 'picture'"
182
+ }).optional(),
183
+ extraFields: z__namespace.record(z__namespace.string(), z__namespace.any()).optional()
230
184
  }).optional(),
231
185
  organizationId: z__namespace.string({}).meta({
232
186
  description: "If organization plugin is enabled, the organization id to link the provider to"
@@ -463,7 +417,7 @@ const sso = (options) => {
463
417
  jwksEndpoint: body.oidcConfig.jwksEndpoint,
464
418
  pkce: body.oidcConfig.pkce,
465
419
  discoveryEndpoint: body.oidcConfig.discoveryEndpoint || `${body.issuer}/.well-known/openid-configuration`,
466
- mapping: body.oidcConfig.mapping,
420
+ mapping: body.mapping,
467
421
  scopes: body.oidcConfig.scopes,
468
422
  userInfoEndpoint: body.oidcConfig.userInfoEndpoint,
469
423
  overrideUserInfo: ctx.body.overrideUserInfo || options?.defaultOverrideUserInfo || false
@@ -483,7 +437,7 @@ const sso = (options) => {
483
437
  privateKey: body.samlConfig.privateKey,
484
438
  decryptionPvk: body.samlConfig.decryptionPvk,
485
439
  additionalParams: body.samlConfig.additionalParams,
486
- mapping: body.samlConfig.mapping
440
+ mapping: body.mapping
487
441
  }) : null,
488
442
  organizationId: body.organizationId,
489
443
  userId: ctx.context.session.user.id,
@@ -607,7 +561,7 @@ const sso = (options) => {
607
561
  async (ctx) => {
608
562
  const body = ctx.body;
609
563
  let { email, organizationSlug, providerId, domain } = body;
610
- if (!options?.defaultSSO?.length && !email && !organizationSlug && !domain && !providerId) {
564
+ if (!email && !organizationSlug && !domain && !providerId) {
611
565
  throw new api.APIError("BAD_REQUEST", {
612
566
  message: "email, organizationSlug, domain or providerId is required"
613
567
  });
@@ -630,48 +584,23 @@ const sso = (options) => {
630
584
  return res.id;
631
585
  });
632
586
  }
633
- let provider = null;
634
- if (options?.defaultSSO?.length) {
635
- const matchingDefault = providerId ? options.defaultSSO.find(
636
- (defaultProvider) => defaultProvider.providerId === providerId
637
- ) : options.defaultSSO.find(
638
- (defaultProvider) => defaultProvider.domain === domain
639
- );
640
- if (matchingDefault) {
641
- provider = {
642
- issuer: matchingDefault.samlConfig?.issuer || matchingDefault.oidcConfig?.issuer || "",
643
- providerId: matchingDefault.providerId,
644
- userId: "default",
645
- oidcConfig: matchingDefault.oidcConfig,
646
- samlConfig: matchingDefault.samlConfig
647
- };
648
- }
649
- }
650
- if (!providerId && !orgId && !domain) {
651
- throw new api.APIError("BAD_REQUEST", {
652
- message: "providerId, orgId or domain is required"
653
- });
654
- }
655
- if (!provider) {
656
- provider = await ctx.context.adapter.findOne({
657
- model: "ssoProvider",
658
- where: [
659
- {
660
- field: providerId ? "providerId" : orgId ? "organizationId" : "domain",
661
- value: providerId || orgId || domain
662
- }
663
- ]
664
- }).then((res) => {
665
- if (!res) {
666
- return null;
587
+ const provider = await ctx.context.adapter.findOne({
588
+ model: "ssoProvider",
589
+ where: [
590
+ {
591
+ field: providerId ? "providerId" : orgId ? "organizationId" : "domain",
592
+ value: providerId || orgId || domain
667
593
  }
668
- return {
669
- ...res,
670
- oidcConfig: res.oidcConfig ? JSON.parse(res.oidcConfig) : void 0,
671
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
672
- };
673
- });
674
- }
594
+ ]
595
+ }).then((res) => {
596
+ if (!res) {
597
+ return null;
598
+ }
599
+ return {
600
+ ...res,
601
+ oidcConfig: JSON.parse(res.oidcConfig)
602
+ };
603
+ });
675
604
  if (!provider) {
676
605
  throw new api.APIError("NOT_FOUND", {
677
606
  message: "No provider found for the issuer"
@@ -715,16 +644,15 @@ const sso = (options) => {
715
644
  });
716
645
  }
717
646
  if (provider.samlConfig) {
718
- const parsedSamlConfig = typeof provider.samlConfig === "object" ? provider.samlConfig : JSON.parse(provider.samlConfig);
647
+ const parsedSamlConfig = JSON.parse(
648
+ provider.samlConfig
649
+ );
719
650
  const sp = saml__namespace.ServiceProvider({
720
651
  metadata: parsedSamlConfig.spMetadata.metadata,
721
652
  allowCreate: true
722
653
  });
723
654
  const idp = saml__namespace.IdentityProvider({
724
- metadata: parsedSamlConfig.idpMetadata?.metadata,
725
- entityID: parsedSamlConfig.idpMetadata?.entityID,
726
- encryptCert: parsedSamlConfig.idpMetadata?.cert,
727
- singleSignOnService: parsedSamlConfig.idpMetadata?.singleSignOnService
655
+ metadata: parsedSamlConfig.idpMetadata.metadata
728
656
  });
729
657
  const loginRequest = sp.createLoginRequest(
730
658
  idp,
@@ -783,38 +711,23 @@ const sso = (options) => {
783
711
  `${errorURL || callbackURL}?error=${error}&error_description=${error_description}`
784
712
  );
785
713
  }
786
- let provider = null;
787
- if (options?.defaultSSO?.length) {
788
- const matchingDefault = options.defaultSSO.find(
789
- (defaultProvider) => defaultProvider.providerId === ctx.params.providerId
790
- );
791
- if (matchingDefault) {
792
- provider = {
793
- ...matchingDefault,
794
- issuer: matchingDefault.oidcConfig?.issuer || "",
795
- userId: "default"
796
- };
797
- }
798
- }
799
- if (!provider) {
800
- provider = await ctx.context.adapter.findOne({
801
- model: "ssoProvider",
802
- where: [
803
- {
804
- field: "providerId",
805
- value: ctx.params.providerId
806
- }
807
- ]
808
- }).then((res) => {
809
- if (!res) {
810
- return null;
714
+ const provider = await ctx.context.adapter.findOne({
715
+ model: "ssoProvider",
716
+ where: [
717
+ {
718
+ field: "providerId",
719
+ value: ctx.params.providerId
811
720
  }
812
- return {
813
- ...res,
814
- oidcConfig: JSON.parse(res.oidcConfig)
815
- };
816
- });
817
- }
721
+ ]
722
+ }).then((res) => {
723
+ if (!res) {
724
+ return null;
725
+ }
726
+ return {
727
+ ...res,
728
+ oidcConfig: JSON.parse(res.oidcConfig)
729
+ };
730
+ });
818
731
  if (!provider) {
819
732
  throw ctx.redirect(
820
733
  `${errorURL || callbackURL}/error?error=invalid_provider&error_description=provider not found`
@@ -1038,31 +951,10 @@ const sso = (options) => {
1038
951
  async (ctx) => {
1039
952
  const { SAMLResponse, RelayState } = ctx.body;
1040
953
  const { providerId } = ctx.params;
1041
- let provider = null;
1042
- if (options?.defaultSSO?.length) {
1043
- const matchingDefault = options.defaultSSO.find(
1044
- (defaultProvider) => defaultProvider.providerId === providerId
1045
- );
1046
- if (matchingDefault) {
1047
- provider = {
1048
- ...matchingDefault,
1049
- userId: "default",
1050
- issuer: matchingDefault.samlConfig?.issuer || ""
1051
- };
1052
- }
1053
- }
1054
- if (!provider) {
1055
- provider = await ctx.context.adapter.findOne({
1056
- model: "ssoProvider",
1057
- where: [{ field: "providerId", value: providerId }]
1058
- }).then((res) => {
1059
- if (!res) return null;
1060
- return {
1061
- ...res,
1062
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
1063
- };
1064
- });
1065
- }
954
+ const provider = await ctx.context.adapter.findOne({
955
+ model: "ssoProvider",
956
+ where: [{ field: "providerId", value: providerId }]
957
+ });
1066
958
  if (!provider) {
1067
959
  throw new api.APIError("NOT_FOUND", {
1068
960
  message: "No provider found for the given providerId"
@@ -1071,389 +963,46 @@ const sso = (options) => {
1071
963
  const parsedSamlConfig = JSON.parse(
1072
964
  provider.samlConfig
1073
965
  );
1074
- const idpData = parsedSamlConfig.idpMetadata;
1075
- let idp = null;
1076
- if (!idpData?.metadata) {
1077
- idp = saml__namespace.IdentityProvider({
1078
- entityID: idpData.entityID || parsedSamlConfig.issuer,
1079
- singleSignOnService: [
1080
- {
1081
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1082
- Location: parsedSamlConfig.entryPoint
1083
- }
1084
- ],
1085
- signingCert: idpData.cert || parsedSamlConfig.cert,
1086
- wantAuthnRequestsSigned: parsedSamlConfig.wantAssertionsSigned || false,
1087
- isAssertionEncrypted: idpData.isAssertionEncrypted || false,
1088
- encPrivateKey: idpData.encPrivateKey,
1089
- encPrivateKeyPass: idpData.encPrivateKeyPass
1090
- });
1091
- } else {
1092
- idp = saml__namespace.IdentityProvider({
1093
- metadata: idpData.metadata,
1094
- privateKey: idpData.privateKey,
1095
- privateKeyPass: idpData.privateKeyPass,
1096
- isAssertionEncrypted: idpData.isAssertionEncrypted,
1097
- encPrivateKey: idpData.encPrivateKey,
1098
- encPrivateKeyPass: idpData.encPrivateKeyPass
1099
- });
1100
- }
1101
- const spData = parsedSamlConfig.spMetadata;
1102
- const sp = saml__namespace.ServiceProvider({
1103
- metadata: spData?.metadata,
1104
- entityID: spData?.entityID || parsedSamlConfig.issuer,
1105
- assertionConsumerService: spData?.metadata ? void 0 : [
1106
- {
1107
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
1108
- Location: parsedSamlConfig.callbackUrl
1109
- }
1110
- ],
1111
- privateKey: spData?.privateKey || parsedSamlConfig.privateKey,
1112
- privateKeyPass: spData?.privateKeyPass,
1113
- isAssertionEncrypted: spData?.isAssertionEncrypted || false,
1114
- encPrivateKey: spData?.encPrivateKey,
1115
- encPrivateKeyPass: spData?.encPrivateKeyPass,
1116
- wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
1117
- nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
1118
- });
1119
- let parsedResponse;
1120
- try {
1121
- const decodedResponse = Buffer.from(
1122
- SAMLResponse,
1123
- "base64"
1124
- ).toString("utf-8");
1125
- try {
1126
- parsedResponse = await sp.parseLoginResponse(idp, "post", {
1127
- body: {
1128
- SAMLResponse,
1129
- RelayState: RelayState || void 0
1130
- }
1131
- });
1132
- } catch (parseError) {
1133
- const nameIDMatch = decodedResponse.match(
1134
- /<saml2:NameID[^>]*>([^<]+)<\/saml2:NameID>/
1135
- );
1136
- if (!nameIDMatch) throw parseError;
1137
- parsedResponse = {
1138
- extract: {
1139
- nameID: nameIDMatch[1],
1140
- attributes: { nameID: nameIDMatch[1] },
1141
- sessionIndex: {},
1142
- conditions: {}
1143
- }
1144
- };
1145
- }
1146
- if (!parsedResponse?.extract) {
1147
- throw new Error("Invalid SAML response structure");
1148
- }
1149
- } catch (error) {
1150
- ctx.context.logger.error("SAML response validation failed", {
1151
- error,
1152
- decodedResponse: Buffer.from(SAMLResponse, "base64").toString(
1153
- "utf-8"
1154
- )
1155
- });
1156
- throw new api.APIError("BAD_REQUEST", {
1157
- message: "Invalid SAML response",
1158
- details: error instanceof Error ? error.message : String(error)
1159
- });
1160
- }
1161
- const { extract } = parsedResponse;
1162
- const attributes = extract.attributes || {};
1163
- const mapping = parsedSamlConfig.mapping ?? {};
1164
- const userInfo = {
1165
- ...Object.fromEntries(
1166
- Object.entries(mapping.extraFields || {}).map(([key, value]) => [
1167
- key,
1168
- attributes[value]
1169
- ])
1170
- ),
1171
- id: attributes[mapping.id || "nameID"] || extract.nameID,
1172
- email: attributes[mapping.email || "email"] || extract.nameID,
1173
- name: [
1174
- attributes[mapping.firstName || "givenName"],
1175
- attributes[mapping.lastName || "surname"]
1176
- ].filter(Boolean).join(" ") || attributes[mapping.name || "displayName"] || extract.nameID,
1177
- emailVerified: options?.trustEmailVerified && mapping.emailVerified ? attributes[mapping.emailVerified] || false : false
1178
- };
1179
- if (!userInfo.id || !userInfo.email) {
1180
- ctx.context.logger.error(
1181
- "Missing essential user info from SAML response",
1182
- {
1183
- attributes: Object.keys(attributes),
1184
- mapping,
1185
- extractedId: userInfo.id,
1186
- extractedEmail: userInfo.email
1187
- }
1188
- );
1189
- throw new api.APIError("BAD_REQUEST", {
1190
- message: "Unable to extract user ID or email from SAML response"
1191
- });
1192
- }
1193
- let user;
1194
- const existingUser = await ctx.context.adapter.findOne({
1195
- model: "user",
1196
- where: [
1197
- {
1198
- field: "email",
1199
- value: userInfo.email
1200
- }
1201
- ]
1202
- });
1203
- if (existingUser) {
1204
- user = existingUser;
1205
- } else {
1206
- user = await ctx.context.adapter.create({
1207
- model: "user",
1208
- data: {
1209
- email: userInfo.email,
1210
- name: userInfo.name,
1211
- emailVerified: userInfo.emailVerified,
1212
- createdAt: /* @__PURE__ */ new Date(),
1213
- updatedAt: /* @__PURE__ */ new Date()
1214
- }
1215
- });
1216
- }
1217
- const account = await ctx.context.adapter.findOne({
1218
- model: "account",
1219
- where: [
1220
- { field: "userId", value: user.id },
1221
- { field: "providerId", value: provider.providerId },
1222
- { field: "accountId", value: userInfo.id }
1223
- ]
966
+ const idp = saml__namespace.IdentityProvider({
967
+ metadata: parsedSamlConfig.idpMetadata.metadata
1224
968
  });
1225
- if (!account) {
1226
- await ctx.context.adapter.create({
1227
- model: "account",
1228
- data: {
1229
- userId: user.id,
1230
- providerId: provider.providerId,
1231
- accountId: userInfo.id,
1232
- createdAt: /* @__PURE__ */ new Date(),
1233
- updatedAt: /* @__PURE__ */ new Date(),
1234
- accessToken: "",
1235
- refreshToken: ""
1236
- }
1237
- });
1238
- }
1239
- if (options?.provisionUser) {
1240
- await options.provisionUser({
1241
- user,
1242
- userInfo,
1243
- provider
1244
- });
1245
- }
1246
- if (provider.organizationId && !options?.organizationProvisioning?.disabled) {
1247
- const isOrgPluginEnabled = ctx.context.options.plugins?.find(
1248
- (plugin) => plugin.id === "organization"
1249
- );
1250
- if (isOrgPluginEnabled) {
1251
- const isAlreadyMember = await ctx.context.adapter.findOne({
1252
- model: "member",
1253
- where: [
1254
- { field: "organizationId", value: provider.organizationId },
1255
- { field: "userId", value: user.id }
1256
- ]
1257
- });
1258
- if (!isAlreadyMember) {
1259
- const role = options?.organizationProvisioning?.getRole ? await options.organizationProvisioning.getRole({
1260
- user,
1261
- userInfo,
1262
- provider
1263
- }) : options?.organizationProvisioning?.defaultRole || "member";
1264
- await ctx.context.adapter.create({
1265
- model: "member",
1266
- data: {
1267
- organizationId: provider.organizationId,
1268
- userId: user.id,
1269
- role,
1270
- createdAt: /* @__PURE__ */ new Date(),
1271
- updatedAt: /* @__PURE__ */ new Date()
1272
- }
1273
- });
1274
- }
1275
- }
1276
- }
1277
- let session = await ctx.context.internalAdapter.createSession(user.id, ctx);
1278
- await cookies.setSessionCookie(ctx, { session, user });
1279
- const callbackUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
1280
- throw ctx.redirect(callbackUrl);
1281
- }
1282
- ),
1283
- acsEndpoint: plugins.createAuthEndpoint(
1284
- "/sso/saml2/sp/acs/:providerId",
1285
- {
1286
- method: "POST",
1287
- params: z__namespace.object({
1288
- providerId: z__namespace.string().optional()
1289
- }),
1290
- body: z__namespace.object({
1291
- SAMLResponse: z__namespace.string(),
1292
- RelayState: z__namespace.string().optional()
1293
- }),
1294
- metadata: {
1295
- isAction: false,
1296
- openapi: {
1297
- summary: "SAML Assertion Consumer Service",
1298
- description: "Handles SAML responses from IdP after successful authentication",
1299
- responses: {
1300
- "302": {
1301
- description: "Redirects to the callback URL after successful authentication"
1302
- }
1303
- }
1304
- }
1305
- }
1306
- },
1307
- async (ctx) => {
1308
- const { SAMLResponse, RelayState = "" } = ctx.body;
1309
- const { providerId } = ctx.params;
1310
- let provider = null;
1311
- if (options?.defaultSSO?.length) {
1312
- const matchingDefault = providerId ? options.defaultSSO.find(
1313
- (defaultProvider) => defaultProvider.providerId === providerId
1314
- ) : options.defaultSSO[0];
1315
- if (matchingDefault) {
1316
- provider = {
1317
- issuer: matchingDefault.samlConfig?.issuer || "",
1318
- providerId: matchingDefault.providerId,
1319
- userId: "default",
1320
- samlConfig: matchingDefault.samlConfig
1321
- };
1322
- }
1323
- } else {
1324
- provider = await ctx.context.adapter.findOne({
1325
- model: "ssoProvider",
1326
- where: [
1327
- {
1328
- field: "providerId",
1329
- value: providerId ?? "sso"
1330
- }
1331
- ]
1332
- }).then((res) => {
1333
- if (!res) return null;
1334
- return {
1335
- ...res,
1336
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
1337
- };
1338
- });
1339
- }
1340
- if (!provider?.samlConfig) {
1341
- throw new api.APIError("NOT_FOUND", {
1342
- message: "No SAML provider found"
1343
- });
1344
- }
1345
- const parsedSamlConfig = provider.samlConfig;
1346
969
  const sp = saml__namespace.ServiceProvider({
1347
- entityID: parsedSamlConfig.spMetadata?.entityID || parsedSamlConfig.issuer,
1348
- assertionConsumerService: [
1349
- {
1350
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
1351
- Location: parsedSamlConfig.callbackUrl || `${ctx.context.baseURL}/sso/saml2/sp/acs/${providerId}`
1352
- }
1353
- ],
1354
- wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
1355
- metadata: parsedSamlConfig.spMetadata?.metadata,
1356
- privateKey: parsedSamlConfig.spMetadata?.privateKey || parsedSamlConfig.privateKey,
1357
- privateKeyPass: parsedSamlConfig.spMetadata?.privateKeyPass,
1358
- nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
1359
- });
1360
- const idpData = parsedSamlConfig.idpMetadata;
1361
- const idp = !idpData?.metadata ? saml__namespace.IdentityProvider({
1362
- entityID: idpData?.entityID || parsedSamlConfig.issuer,
1363
- singleSignOnService: idpData?.singleSignOnService || [
1364
- {
1365
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1366
- Location: parsedSamlConfig.entryPoint
1367
- }
1368
- ],
1369
- signingCert: idpData?.cert || parsedSamlConfig.cert
1370
- }) : saml__namespace.IdentityProvider({
1371
- metadata: idpData.metadata
970
+ metadata: parsedSamlConfig.spMetadata.metadata
1372
971
  });
1373
972
  let parsedResponse;
1374
973
  try {
1375
- let decodedResponse = Buffer.from(SAMLResponse, "base64").toString(
1376
- "utf-8"
1377
- );
1378
- if (!decodedResponse.includes("StatusCode")) {
1379
- const insertPoint = decodedResponse.indexOf("</saml2:Issuer>");
1380
- if (insertPoint !== -1) {
1381
- decodedResponse = decodedResponse.slice(0, insertPoint + 14) + '<saml2:Status><saml2:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></saml2:Status>' + decodedResponse.slice(insertPoint + 14);
1382
- }
1383
- } else if (!decodedResponse.includes("saml2:Success")) {
1384
- decodedResponse = decodedResponse.replace(
1385
- /<saml2:StatusCode Value="[^"]+"/,
1386
- '<saml2:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"'
1387
- );
1388
- }
1389
- try {
1390
- parsedResponse = await sp.parseLoginResponse(idp, "post", {
1391
- body: {
1392
- SAMLResponse,
1393
- RelayState: RelayState || void 0
1394
- }
1395
- });
1396
- } catch (parseError) {
1397
- const nameIDMatch = decodedResponse.match(
1398
- /<saml2:NameID[^>]*>([^<]+)<\/saml2:NameID>/
1399
- );
1400
- if (!nameIDMatch) throw parseError;
1401
- parsedResponse = {
1402
- extract: {
1403
- nameID: nameIDMatch[1],
1404
- attributes: { nameID: nameIDMatch[1] },
1405
- sessionIndex: {},
1406
- conditions: {}
1407
- }
1408
- };
1409
- }
1410
- if (!parsedResponse?.extract) {
1411
- throw new Error("Invalid SAML response structure");
974
+ parsedResponse = await sp.parseLoginResponse(idp, "post", {
975
+ body: { SAMLResponse, RelayState }
976
+ });
977
+ if (!parsedResponse) {
978
+ throw new Error("Empty SAML response");
1412
979
  }
1413
980
  } catch (error) {
1414
- ctx.context.logger.error("SAML response validation failed", {
1415
- error,
1416
- decodedResponse: Buffer.from(SAMLResponse, "base64").toString(
1417
- "utf-8"
1418
- )
1419
- });
981
+ ctx.context.logger.error("SAML response validation failed", error);
1420
982
  throw new api.APIError("BAD_REQUEST", {
1421
983
  message: "Invalid SAML response",
1422
984
  details: error instanceof Error ? error.message : String(error)
1423
985
  });
1424
986
  }
1425
987
  const { extract } = parsedResponse;
1426
- const attributes = extract.attributes || {};
1427
- const mapping = parsedSamlConfig.mapping ?? {};
988
+ const attributes = parsedResponse.extract.attributes;
989
+ const mapping = parsedSamlConfig?.mapping ?? {};
1428
990
  const userInfo = {
1429
991
  ...Object.fromEntries(
1430
992
  Object.entries(mapping.extraFields || {}).map(([key, value]) => [
1431
993
  key,
1432
- attributes[value]
994
+ extract.attributes[value]
1433
995
  ])
1434
996
  ),
1435
- id: attributes[mapping.id || "nameID"] || extract.nameID,
1436
- email: attributes[mapping.email || "email"] || extract.nameID,
997
+ id: attributes[mapping.id] || attributes["nameID"],
998
+ email: attributes[mapping.email] || attributes["nameID"] || attributes["email"],
1437
999
  name: [
1438
- attributes[mapping.firstName || "givenName"],
1439
- attributes[mapping.lastName || "surname"]
1440
- ].filter(Boolean).join(" ") || attributes[mapping.name || "displayName"] || extract.nameID,
1441
- emailVerified: options?.trustEmailVerified && mapping.emailVerified ? attributes[mapping.emailVerified] || false : false
1000
+ attributes[mapping.firstName] || attributes["givenName"],
1001
+ attributes[mapping.lastName] || attributes["surname"]
1002
+ ].filter(Boolean).join(" ") || parsedResponse.extract.attributes?.displayName,
1003
+ attributes: parsedResponse.extract.attributes,
1004
+ emailVerified: options?.trustEmailVerified ? attributes?.[mapping.emailVerified] || false : false
1442
1005
  };
1443
- if (!userInfo.id || !userInfo.email) {
1444
- ctx.context.logger.error(
1445
- "Missing essential user info from SAML response",
1446
- {
1447
- attributes: Object.keys(attributes),
1448
- mapping,
1449
- extractedId: userInfo.id,
1450
- extractedEmail: userInfo.email
1451
- }
1452
- );
1453
- throw new api.APIError("BAD_REQUEST", {
1454
- message: "Unable to extract user ID or email from SAML response"
1455
- });
1456
- }
1457
1006
  let user;
1458
1007
  const existingUser = await ctx.context.adapter.findOne({
1459
1008
  model: "user",
@@ -1465,7 +1014,7 @@ const sso = (options) => {
1465
1014
  ]
1466
1015
  });
1467
1016
  if (existingUser) {
1468
- const account = await ctx.context.adapter.findOne({
1017
+ const accounts = await ctx.context.adapter.findOne({
1469
1018
  model: "account",
1470
1019
  where: [
1471
1020
  { field: "userId", value: existingUser.id },
@@ -1473,7 +1022,7 @@ const sso = (options) => {
1473
1022
  { field: "accountId", value: userInfo.id }
1474
1023
  ]
1475
1024
  });
1476
- if (!account) {
1025
+ if (!accounts) {
1477
1026
  const isTrustedProvider = ctx.context.options.account?.accountLinking?.trustedProviders?.includes(
1478
1027
  provider.providerId
1479
1028
  );
@@ -1563,8 +1112,9 @@ const sso = (options) => {
1563
1112
  }
1564
1113
  let session = await ctx.context.internalAdapter.createSession(user.id, ctx);
1565
1114
  await cookies.setSessionCookie(ctx, { session, user });
1566
- const callbackUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
1567
- throw ctx.redirect(callbackUrl);
1115
+ throw ctx.redirect(
1116
+ RelayState || `${parsedSamlConfig.callbackUrl}` || `${parsedSamlConfig.issuer}`
1117
+ );
1568
1118
  }
1569
1119
  )
1570
1120
  },