@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.mjs CHANGED
@@ -59,18 +59,8 @@ const sso = (options) => {
59
59
  });
60
60
  }
61
61
  const parsedSamlConfig = JSON.parse(provider.samlConfig);
62
- const sp = parsedSamlConfig.spMetadata.metadata ? saml.ServiceProvider({
62
+ const sp = saml.ServiceProvider({
63
63
  metadata: parsedSamlConfig.spMetadata.metadata
64
- }) : saml.SPMetadata({
65
- entityID: parsedSamlConfig.spMetadata?.entityID || parsedSamlConfig.issuer,
66
- assertionConsumerService: [
67
- {
68
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
69
- Location: parsedSamlConfig.callbackUrl || `${ctx.context.baseURL}/sso/saml2/sp/acs/${provider.id}`
70
- }
71
- ],
72
- wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
73
- nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
74
64
  });
75
65
  return new Response(sp.getMetadata(), {
76
66
  headers: {
@@ -119,25 +109,7 @@ const sso = (options) => {
119
109
  }).optional(),
120
110
  pkce: z.boolean({}).meta({
121
111
  description: "Whether to use PKCE for the authorization flow"
122
- }).default(true).optional(),
123
- mapping: z.object({
124
- id: z.string({}).meta({
125
- description: "Field mapping for user ID (defaults to 'sub')"
126
- }),
127
- email: z.string({}).meta({
128
- description: "Field mapping for email (defaults to 'email')"
129
- }),
130
- emailVerified: z.string({}).meta({
131
- description: "Field mapping for email verification (defaults to 'email_verified')"
132
- }).optional(),
133
- name: z.string({}).meta({
134
- description: "Field mapping for name (defaults to 'name')"
135
- }),
136
- image: z.string({}).meta({
137
- description: "Field mapping for image (defaults to 'picture')"
138
- }).optional(),
139
- extraFields: z.record(z.string(), z.any()).optional()
140
- }).optional()
112
+ }).default(true).optional()
141
113
  }).optional(),
142
114
  samlConfig: z.object({
143
115
  entryPoint: z.string({}).meta({
@@ -151,30 +123,15 @@ const sso = (options) => {
151
123
  }),
152
124
  audience: z.string().optional(),
153
125
  idpMetadata: z.object({
154
- metadata: z.string().optional(),
155
- entityID: z.string().optional(),
156
- cert: z.string().optional(),
126
+ metadata: z.string(),
157
127
  privateKey: z.string().optional(),
158
128
  privateKeyPass: z.string().optional(),
159
129
  isAssertionEncrypted: z.boolean().optional(),
160
130
  encPrivateKey: z.string().optional(),
161
- encPrivateKeyPass: z.string().optional(),
162
- singleSignOnService: z.array(
163
- z.object({
164
- Binding: z.string().meta({
165
- description: "The binding type for the SSO service"
166
- }),
167
- Location: z.string().meta({
168
- description: "The URL for the SSO service"
169
- })
170
- })
171
- ).optional().meta({
172
- description: "Single Sign-On service configuration"
173
- })
131
+ encPrivateKeyPass: z.string().optional()
174
132
  }).optional(),
175
133
  spMetadata: z.object({
176
- metadata: z.string().optional(),
177
- entityID: z.string().optional(),
134
+ metadata: z.string(),
178
135
  binding: z.string().optional(),
179
136
  privateKey: z.string().optional(),
180
137
  privateKeyPass: z.string().optional(),
@@ -188,28 +145,25 @@ const sso = (options) => {
188
145
  identifierFormat: z.string().optional(),
189
146
  privateKey: z.string().optional(),
190
147
  decryptionPvk: z.string().optional(),
191
- additionalParams: z.record(z.string(), z.any()).optional(),
192
- mapping: z.object({
193
- id: z.string({}).meta({
194
- description: "Field mapping for user ID (defaults to 'nameID')"
195
- }),
196
- email: z.string({}).meta({
197
- description: "Field mapping for email (defaults to 'email')"
198
- }),
199
- emailVerified: z.string({}).meta({
200
- description: "Field mapping for email verification"
201
- }).optional(),
202
- name: z.string({}).meta({
203
- description: "Field mapping for name (defaults to 'displayName')"
204
- }),
205
- firstName: z.string({}).meta({
206
- description: "Field mapping for first name (defaults to 'givenName')"
207
- }).optional(),
208
- lastName: z.string({}).meta({
209
- description: "Field mapping for last name (defaults to 'surname')"
210
- }).optional(),
211
- extraFields: z.record(z.string(), z.any()).optional()
212
- }).optional()
148
+ additionalParams: z.record(z.string(), z.any()).optional()
149
+ }).optional(),
150
+ mapping: z.object({
151
+ id: z.string({}).meta({
152
+ description: "The field in the user info response that contains the id. Defaults to 'sub'"
153
+ }),
154
+ email: z.string({}).meta({
155
+ description: "The field in the user info response that contains the email. Defaults to 'email'"
156
+ }),
157
+ emailVerified: z.string({}).meta({
158
+ description: "The field in the user info response that contains whether the email is verified. defaults to 'email_verified'"
159
+ }).optional(),
160
+ name: z.string({}).meta({
161
+ description: "The field in the user info response that contains the name. Defaults to 'name'"
162
+ }),
163
+ image: z.string({}).meta({
164
+ description: "The field in the user info response that contains the image. Defaults to 'picture'"
165
+ }).optional(),
166
+ extraFields: z.record(z.string(), z.any()).optional()
213
167
  }).optional(),
214
168
  organizationId: z.string({}).meta({
215
169
  description: "If organization plugin is enabled, the organization id to link the provider to"
@@ -446,7 +400,7 @@ const sso = (options) => {
446
400
  jwksEndpoint: body.oidcConfig.jwksEndpoint,
447
401
  pkce: body.oidcConfig.pkce,
448
402
  discoveryEndpoint: body.oidcConfig.discoveryEndpoint || `${body.issuer}/.well-known/openid-configuration`,
449
- mapping: body.oidcConfig.mapping,
403
+ mapping: body.mapping,
450
404
  scopes: body.oidcConfig.scopes,
451
405
  userInfoEndpoint: body.oidcConfig.userInfoEndpoint,
452
406
  overrideUserInfo: ctx.body.overrideUserInfo || options?.defaultOverrideUserInfo || false
@@ -466,7 +420,7 @@ const sso = (options) => {
466
420
  privateKey: body.samlConfig.privateKey,
467
421
  decryptionPvk: body.samlConfig.decryptionPvk,
468
422
  additionalParams: body.samlConfig.additionalParams,
469
- mapping: body.samlConfig.mapping
423
+ mapping: body.mapping
470
424
  }) : null,
471
425
  organizationId: body.organizationId,
472
426
  userId: ctx.context.session.user.id,
@@ -590,7 +544,7 @@ const sso = (options) => {
590
544
  async (ctx) => {
591
545
  const body = ctx.body;
592
546
  let { email, organizationSlug, providerId, domain } = body;
593
- if (!options?.defaultSSO?.length && !email && !organizationSlug && !domain && !providerId) {
547
+ if (!email && !organizationSlug && !domain && !providerId) {
594
548
  throw new APIError("BAD_REQUEST", {
595
549
  message: "email, organizationSlug, domain or providerId is required"
596
550
  });
@@ -613,48 +567,23 @@ const sso = (options) => {
613
567
  return res.id;
614
568
  });
615
569
  }
616
- let provider = null;
617
- if (options?.defaultSSO?.length) {
618
- const matchingDefault = providerId ? options.defaultSSO.find(
619
- (defaultProvider) => defaultProvider.providerId === providerId
620
- ) : options.defaultSSO.find(
621
- (defaultProvider) => defaultProvider.domain === domain
622
- );
623
- if (matchingDefault) {
624
- provider = {
625
- issuer: matchingDefault.samlConfig?.issuer || matchingDefault.oidcConfig?.issuer || "",
626
- providerId: matchingDefault.providerId,
627
- userId: "default",
628
- oidcConfig: matchingDefault.oidcConfig,
629
- samlConfig: matchingDefault.samlConfig
630
- };
631
- }
632
- }
633
- if (!providerId && !orgId && !domain) {
634
- throw new APIError("BAD_REQUEST", {
635
- message: "providerId, orgId or domain is required"
636
- });
637
- }
638
- if (!provider) {
639
- provider = await ctx.context.adapter.findOne({
640
- model: "ssoProvider",
641
- where: [
642
- {
643
- field: providerId ? "providerId" : orgId ? "organizationId" : "domain",
644
- value: providerId || orgId || domain
645
- }
646
- ]
647
- }).then((res) => {
648
- if (!res) {
649
- return null;
570
+ const provider = await ctx.context.adapter.findOne({
571
+ model: "ssoProvider",
572
+ where: [
573
+ {
574
+ field: providerId ? "providerId" : orgId ? "organizationId" : "domain",
575
+ value: providerId || orgId || domain
650
576
  }
651
- return {
652
- ...res,
653
- oidcConfig: res.oidcConfig ? JSON.parse(res.oidcConfig) : void 0,
654
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
655
- };
656
- });
657
- }
577
+ ]
578
+ }).then((res) => {
579
+ if (!res) {
580
+ return null;
581
+ }
582
+ return {
583
+ ...res,
584
+ oidcConfig: JSON.parse(res.oidcConfig)
585
+ };
586
+ });
658
587
  if (!provider) {
659
588
  throw new APIError("NOT_FOUND", {
660
589
  message: "No provider found for the issuer"
@@ -698,16 +627,15 @@ const sso = (options) => {
698
627
  });
699
628
  }
700
629
  if (provider.samlConfig) {
701
- const parsedSamlConfig = typeof provider.samlConfig === "object" ? provider.samlConfig : JSON.parse(provider.samlConfig);
630
+ const parsedSamlConfig = JSON.parse(
631
+ provider.samlConfig
632
+ );
702
633
  const sp = saml.ServiceProvider({
703
634
  metadata: parsedSamlConfig.spMetadata.metadata,
704
635
  allowCreate: true
705
636
  });
706
637
  const idp = saml.IdentityProvider({
707
- metadata: parsedSamlConfig.idpMetadata?.metadata,
708
- entityID: parsedSamlConfig.idpMetadata?.entityID,
709
- encryptCert: parsedSamlConfig.idpMetadata?.cert,
710
- singleSignOnService: parsedSamlConfig.idpMetadata?.singleSignOnService
638
+ metadata: parsedSamlConfig.idpMetadata.metadata
711
639
  });
712
640
  const loginRequest = sp.createLoginRequest(
713
641
  idp,
@@ -766,38 +694,23 @@ const sso = (options) => {
766
694
  `${errorURL || callbackURL}?error=${error}&error_description=${error_description}`
767
695
  );
768
696
  }
769
- let provider = null;
770
- if (options?.defaultSSO?.length) {
771
- const matchingDefault = options.defaultSSO.find(
772
- (defaultProvider) => defaultProvider.providerId === ctx.params.providerId
773
- );
774
- if (matchingDefault) {
775
- provider = {
776
- ...matchingDefault,
777
- issuer: matchingDefault.oidcConfig?.issuer || "",
778
- userId: "default"
779
- };
780
- }
781
- }
782
- if (!provider) {
783
- provider = await ctx.context.adapter.findOne({
784
- model: "ssoProvider",
785
- where: [
786
- {
787
- field: "providerId",
788
- value: ctx.params.providerId
789
- }
790
- ]
791
- }).then((res) => {
792
- if (!res) {
793
- return null;
697
+ const provider = await ctx.context.adapter.findOne({
698
+ model: "ssoProvider",
699
+ where: [
700
+ {
701
+ field: "providerId",
702
+ value: ctx.params.providerId
794
703
  }
795
- return {
796
- ...res,
797
- oidcConfig: JSON.parse(res.oidcConfig)
798
- };
799
- });
800
- }
704
+ ]
705
+ }).then((res) => {
706
+ if (!res) {
707
+ return null;
708
+ }
709
+ return {
710
+ ...res,
711
+ oidcConfig: JSON.parse(res.oidcConfig)
712
+ };
713
+ });
801
714
  if (!provider) {
802
715
  throw ctx.redirect(
803
716
  `${errorURL || callbackURL}/error?error=invalid_provider&error_description=provider not found`
@@ -1021,31 +934,10 @@ const sso = (options) => {
1021
934
  async (ctx) => {
1022
935
  const { SAMLResponse, RelayState } = ctx.body;
1023
936
  const { providerId } = ctx.params;
1024
- let provider = null;
1025
- if (options?.defaultSSO?.length) {
1026
- const matchingDefault = options.defaultSSO.find(
1027
- (defaultProvider) => defaultProvider.providerId === providerId
1028
- );
1029
- if (matchingDefault) {
1030
- provider = {
1031
- ...matchingDefault,
1032
- userId: "default",
1033
- issuer: matchingDefault.samlConfig?.issuer || ""
1034
- };
1035
- }
1036
- }
1037
- if (!provider) {
1038
- provider = await ctx.context.adapter.findOne({
1039
- model: "ssoProvider",
1040
- where: [{ field: "providerId", value: providerId }]
1041
- }).then((res) => {
1042
- if (!res) return null;
1043
- return {
1044
- ...res,
1045
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
1046
- };
1047
- });
1048
- }
937
+ const provider = await ctx.context.adapter.findOne({
938
+ model: "ssoProvider",
939
+ where: [{ field: "providerId", value: providerId }]
940
+ });
1049
941
  if (!provider) {
1050
942
  throw new APIError("NOT_FOUND", {
1051
943
  message: "No provider found for the given providerId"
@@ -1054,389 +946,46 @@ const sso = (options) => {
1054
946
  const parsedSamlConfig = JSON.parse(
1055
947
  provider.samlConfig
1056
948
  );
1057
- const idpData = parsedSamlConfig.idpMetadata;
1058
- let idp = null;
1059
- if (!idpData?.metadata) {
1060
- idp = saml.IdentityProvider({
1061
- entityID: idpData.entityID || parsedSamlConfig.issuer,
1062
- singleSignOnService: [
1063
- {
1064
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1065
- Location: parsedSamlConfig.entryPoint
1066
- }
1067
- ],
1068
- signingCert: idpData.cert || parsedSamlConfig.cert,
1069
- wantAuthnRequestsSigned: parsedSamlConfig.wantAssertionsSigned || false,
1070
- isAssertionEncrypted: idpData.isAssertionEncrypted || false,
1071
- encPrivateKey: idpData.encPrivateKey,
1072
- encPrivateKeyPass: idpData.encPrivateKeyPass
1073
- });
1074
- } else {
1075
- idp = saml.IdentityProvider({
1076
- metadata: idpData.metadata,
1077
- privateKey: idpData.privateKey,
1078
- privateKeyPass: idpData.privateKeyPass,
1079
- isAssertionEncrypted: idpData.isAssertionEncrypted,
1080
- encPrivateKey: idpData.encPrivateKey,
1081
- encPrivateKeyPass: idpData.encPrivateKeyPass
1082
- });
1083
- }
1084
- const spData = parsedSamlConfig.spMetadata;
1085
- const sp = saml.ServiceProvider({
1086
- metadata: spData?.metadata,
1087
- entityID: spData?.entityID || parsedSamlConfig.issuer,
1088
- assertionConsumerService: spData?.metadata ? void 0 : [
1089
- {
1090
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
1091
- Location: parsedSamlConfig.callbackUrl
1092
- }
1093
- ],
1094
- privateKey: spData?.privateKey || parsedSamlConfig.privateKey,
1095
- privateKeyPass: spData?.privateKeyPass,
1096
- isAssertionEncrypted: spData?.isAssertionEncrypted || false,
1097
- encPrivateKey: spData?.encPrivateKey,
1098
- encPrivateKeyPass: spData?.encPrivateKeyPass,
1099
- wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
1100
- nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
1101
- });
1102
- let parsedResponse;
1103
- try {
1104
- const decodedResponse = Buffer.from(
1105
- SAMLResponse,
1106
- "base64"
1107
- ).toString("utf-8");
1108
- try {
1109
- parsedResponse = await sp.parseLoginResponse(idp, "post", {
1110
- body: {
1111
- SAMLResponse,
1112
- RelayState: RelayState || void 0
1113
- }
1114
- });
1115
- } catch (parseError) {
1116
- const nameIDMatch = decodedResponse.match(
1117
- /<saml2:NameID[^>]*>([^<]+)<\/saml2:NameID>/
1118
- );
1119
- if (!nameIDMatch) throw parseError;
1120
- parsedResponse = {
1121
- extract: {
1122
- nameID: nameIDMatch[1],
1123
- attributes: { nameID: nameIDMatch[1] },
1124
- sessionIndex: {},
1125
- conditions: {}
1126
- }
1127
- };
1128
- }
1129
- if (!parsedResponse?.extract) {
1130
- throw new Error("Invalid SAML response structure");
1131
- }
1132
- } catch (error) {
1133
- ctx.context.logger.error("SAML response validation failed", {
1134
- error,
1135
- decodedResponse: Buffer.from(SAMLResponse, "base64").toString(
1136
- "utf-8"
1137
- )
1138
- });
1139
- throw new APIError("BAD_REQUEST", {
1140
- message: "Invalid SAML response",
1141
- details: error instanceof Error ? error.message : String(error)
1142
- });
1143
- }
1144
- const { extract } = parsedResponse;
1145
- const attributes = extract.attributes || {};
1146
- const mapping = parsedSamlConfig.mapping ?? {};
1147
- const userInfo = {
1148
- ...Object.fromEntries(
1149
- Object.entries(mapping.extraFields || {}).map(([key, value]) => [
1150
- key,
1151
- attributes[value]
1152
- ])
1153
- ),
1154
- id: attributes[mapping.id || "nameID"] || extract.nameID,
1155
- email: attributes[mapping.email || "email"] || extract.nameID,
1156
- name: [
1157
- attributes[mapping.firstName || "givenName"],
1158
- attributes[mapping.lastName || "surname"]
1159
- ].filter(Boolean).join(" ") || attributes[mapping.name || "displayName"] || extract.nameID,
1160
- emailVerified: options?.trustEmailVerified && mapping.emailVerified ? attributes[mapping.emailVerified] || false : false
1161
- };
1162
- if (!userInfo.id || !userInfo.email) {
1163
- ctx.context.logger.error(
1164
- "Missing essential user info from SAML response",
1165
- {
1166
- attributes: Object.keys(attributes),
1167
- mapping,
1168
- extractedId: userInfo.id,
1169
- extractedEmail: userInfo.email
1170
- }
1171
- );
1172
- throw new APIError("BAD_REQUEST", {
1173
- message: "Unable to extract user ID or email from SAML response"
1174
- });
1175
- }
1176
- let user;
1177
- const existingUser = await ctx.context.adapter.findOne({
1178
- model: "user",
1179
- where: [
1180
- {
1181
- field: "email",
1182
- value: userInfo.email
1183
- }
1184
- ]
1185
- });
1186
- if (existingUser) {
1187
- user = existingUser;
1188
- } else {
1189
- user = await ctx.context.adapter.create({
1190
- model: "user",
1191
- data: {
1192
- email: userInfo.email,
1193
- name: userInfo.name,
1194
- emailVerified: userInfo.emailVerified,
1195
- createdAt: /* @__PURE__ */ new Date(),
1196
- updatedAt: /* @__PURE__ */ new Date()
1197
- }
1198
- });
1199
- }
1200
- const account = await ctx.context.adapter.findOne({
1201
- model: "account",
1202
- where: [
1203
- { field: "userId", value: user.id },
1204
- { field: "providerId", value: provider.providerId },
1205
- { field: "accountId", value: userInfo.id }
1206
- ]
949
+ const idp = saml.IdentityProvider({
950
+ metadata: parsedSamlConfig.idpMetadata.metadata
1207
951
  });
1208
- if (!account) {
1209
- await ctx.context.adapter.create({
1210
- model: "account",
1211
- data: {
1212
- userId: user.id,
1213
- providerId: provider.providerId,
1214
- accountId: userInfo.id,
1215
- createdAt: /* @__PURE__ */ new Date(),
1216
- updatedAt: /* @__PURE__ */ new Date(),
1217
- accessToken: "",
1218
- refreshToken: ""
1219
- }
1220
- });
1221
- }
1222
- if (options?.provisionUser) {
1223
- await options.provisionUser({
1224
- user,
1225
- userInfo,
1226
- provider
1227
- });
1228
- }
1229
- if (provider.organizationId && !options?.organizationProvisioning?.disabled) {
1230
- const isOrgPluginEnabled = ctx.context.options.plugins?.find(
1231
- (plugin) => plugin.id === "organization"
1232
- );
1233
- if (isOrgPluginEnabled) {
1234
- const isAlreadyMember = await ctx.context.adapter.findOne({
1235
- model: "member",
1236
- where: [
1237
- { field: "organizationId", value: provider.organizationId },
1238
- { field: "userId", value: user.id }
1239
- ]
1240
- });
1241
- if (!isAlreadyMember) {
1242
- const role = options?.organizationProvisioning?.getRole ? await options.organizationProvisioning.getRole({
1243
- user,
1244
- userInfo,
1245
- provider
1246
- }) : options?.organizationProvisioning?.defaultRole || "member";
1247
- await ctx.context.adapter.create({
1248
- model: "member",
1249
- data: {
1250
- organizationId: provider.organizationId,
1251
- userId: user.id,
1252
- role,
1253
- createdAt: /* @__PURE__ */ new Date(),
1254
- updatedAt: /* @__PURE__ */ new Date()
1255
- }
1256
- });
1257
- }
1258
- }
1259
- }
1260
- let session = await ctx.context.internalAdapter.createSession(user.id, ctx);
1261
- await setSessionCookie(ctx, { session, user });
1262
- const callbackUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
1263
- throw ctx.redirect(callbackUrl);
1264
- }
1265
- ),
1266
- acsEndpoint: createAuthEndpoint(
1267
- "/sso/saml2/sp/acs/:providerId",
1268
- {
1269
- method: "POST",
1270
- params: z.object({
1271
- providerId: z.string().optional()
1272
- }),
1273
- body: z.object({
1274
- SAMLResponse: z.string(),
1275
- RelayState: z.string().optional()
1276
- }),
1277
- metadata: {
1278
- isAction: false,
1279
- openapi: {
1280
- summary: "SAML Assertion Consumer Service",
1281
- description: "Handles SAML responses from IdP after successful authentication",
1282
- responses: {
1283
- "302": {
1284
- description: "Redirects to the callback URL after successful authentication"
1285
- }
1286
- }
1287
- }
1288
- }
1289
- },
1290
- async (ctx) => {
1291
- const { SAMLResponse, RelayState = "" } = ctx.body;
1292
- const { providerId } = ctx.params;
1293
- let provider = null;
1294
- if (options?.defaultSSO?.length) {
1295
- const matchingDefault = providerId ? options.defaultSSO.find(
1296
- (defaultProvider) => defaultProvider.providerId === providerId
1297
- ) : options.defaultSSO[0];
1298
- if (matchingDefault) {
1299
- provider = {
1300
- issuer: matchingDefault.samlConfig?.issuer || "",
1301
- providerId: matchingDefault.providerId,
1302
- userId: "default",
1303
- samlConfig: matchingDefault.samlConfig
1304
- };
1305
- }
1306
- } else {
1307
- provider = await ctx.context.adapter.findOne({
1308
- model: "ssoProvider",
1309
- where: [
1310
- {
1311
- field: "providerId",
1312
- value: providerId ?? "sso"
1313
- }
1314
- ]
1315
- }).then((res) => {
1316
- if (!res) return null;
1317
- return {
1318
- ...res,
1319
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
1320
- };
1321
- });
1322
- }
1323
- if (!provider?.samlConfig) {
1324
- throw new APIError("NOT_FOUND", {
1325
- message: "No SAML provider found"
1326
- });
1327
- }
1328
- const parsedSamlConfig = provider.samlConfig;
1329
952
  const sp = saml.ServiceProvider({
1330
- entityID: parsedSamlConfig.spMetadata?.entityID || parsedSamlConfig.issuer,
1331
- assertionConsumerService: [
1332
- {
1333
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
1334
- Location: parsedSamlConfig.callbackUrl || `${ctx.context.baseURL}/sso/saml2/sp/acs/${providerId}`
1335
- }
1336
- ],
1337
- wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
1338
- metadata: parsedSamlConfig.spMetadata?.metadata,
1339
- privateKey: parsedSamlConfig.spMetadata?.privateKey || parsedSamlConfig.privateKey,
1340
- privateKeyPass: parsedSamlConfig.spMetadata?.privateKeyPass,
1341
- nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
1342
- });
1343
- const idpData = parsedSamlConfig.idpMetadata;
1344
- const idp = !idpData?.metadata ? saml.IdentityProvider({
1345
- entityID: idpData?.entityID || parsedSamlConfig.issuer,
1346
- singleSignOnService: idpData?.singleSignOnService || [
1347
- {
1348
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1349
- Location: parsedSamlConfig.entryPoint
1350
- }
1351
- ],
1352
- signingCert: idpData?.cert || parsedSamlConfig.cert
1353
- }) : saml.IdentityProvider({
1354
- metadata: idpData.metadata
953
+ metadata: parsedSamlConfig.spMetadata.metadata
1355
954
  });
1356
955
  let parsedResponse;
1357
956
  try {
1358
- let decodedResponse = Buffer.from(SAMLResponse, "base64").toString(
1359
- "utf-8"
1360
- );
1361
- if (!decodedResponse.includes("StatusCode")) {
1362
- const insertPoint = decodedResponse.indexOf("</saml2:Issuer>");
1363
- if (insertPoint !== -1) {
1364
- 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);
1365
- }
1366
- } else if (!decodedResponse.includes("saml2:Success")) {
1367
- decodedResponse = decodedResponse.replace(
1368
- /<saml2:StatusCode Value="[^"]+"/,
1369
- '<saml2:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"'
1370
- );
1371
- }
1372
- try {
1373
- parsedResponse = await sp.parseLoginResponse(idp, "post", {
1374
- body: {
1375
- SAMLResponse,
1376
- RelayState: RelayState || void 0
1377
- }
1378
- });
1379
- } catch (parseError) {
1380
- const nameIDMatch = decodedResponse.match(
1381
- /<saml2:NameID[^>]*>([^<]+)<\/saml2:NameID>/
1382
- );
1383
- if (!nameIDMatch) throw parseError;
1384
- parsedResponse = {
1385
- extract: {
1386
- nameID: nameIDMatch[1],
1387
- attributes: { nameID: nameIDMatch[1] },
1388
- sessionIndex: {},
1389
- conditions: {}
1390
- }
1391
- };
1392
- }
1393
- if (!parsedResponse?.extract) {
1394
- throw new Error("Invalid SAML response structure");
957
+ parsedResponse = await sp.parseLoginResponse(idp, "post", {
958
+ body: { SAMLResponse, RelayState }
959
+ });
960
+ if (!parsedResponse) {
961
+ throw new Error("Empty SAML response");
1395
962
  }
1396
963
  } catch (error) {
1397
- ctx.context.logger.error("SAML response validation failed", {
1398
- error,
1399
- decodedResponse: Buffer.from(SAMLResponse, "base64").toString(
1400
- "utf-8"
1401
- )
1402
- });
964
+ ctx.context.logger.error("SAML response validation failed", error);
1403
965
  throw new APIError("BAD_REQUEST", {
1404
966
  message: "Invalid SAML response",
1405
967
  details: error instanceof Error ? error.message : String(error)
1406
968
  });
1407
969
  }
1408
970
  const { extract } = parsedResponse;
1409
- const attributes = extract.attributes || {};
1410
- const mapping = parsedSamlConfig.mapping ?? {};
971
+ const attributes = parsedResponse.extract.attributes;
972
+ const mapping = parsedSamlConfig?.mapping ?? {};
1411
973
  const userInfo = {
1412
974
  ...Object.fromEntries(
1413
975
  Object.entries(mapping.extraFields || {}).map(([key, value]) => [
1414
976
  key,
1415
- attributes[value]
977
+ extract.attributes[value]
1416
978
  ])
1417
979
  ),
1418
- id: attributes[mapping.id || "nameID"] || extract.nameID,
1419
- email: attributes[mapping.email || "email"] || extract.nameID,
980
+ id: attributes[mapping.id] || attributes["nameID"],
981
+ email: attributes[mapping.email] || attributes["nameID"] || attributes["email"],
1420
982
  name: [
1421
- attributes[mapping.firstName || "givenName"],
1422
- attributes[mapping.lastName || "surname"]
1423
- ].filter(Boolean).join(" ") || attributes[mapping.name || "displayName"] || extract.nameID,
1424
- emailVerified: options?.trustEmailVerified && mapping.emailVerified ? attributes[mapping.emailVerified] || false : false
983
+ attributes[mapping.firstName] || attributes["givenName"],
984
+ attributes[mapping.lastName] || attributes["surname"]
985
+ ].filter(Boolean).join(" ") || parsedResponse.extract.attributes?.displayName,
986
+ attributes: parsedResponse.extract.attributes,
987
+ emailVerified: options?.trustEmailVerified ? attributes?.[mapping.emailVerified] || false : false
1425
988
  };
1426
- if (!userInfo.id || !userInfo.email) {
1427
- ctx.context.logger.error(
1428
- "Missing essential user info from SAML response",
1429
- {
1430
- attributes: Object.keys(attributes),
1431
- mapping,
1432
- extractedId: userInfo.id,
1433
- extractedEmail: userInfo.email
1434
- }
1435
- );
1436
- throw new APIError("BAD_REQUEST", {
1437
- message: "Unable to extract user ID or email from SAML response"
1438
- });
1439
- }
1440
989
  let user;
1441
990
  const existingUser = await ctx.context.adapter.findOne({
1442
991
  model: "user",
@@ -1448,7 +997,7 @@ const sso = (options) => {
1448
997
  ]
1449
998
  });
1450
999
  if (existingUser) {
1451
- const account = await ctx.context.adapter.findOne({
1000
+ const accounts = await ctx.context.adapter.findOne({
1452
1001
  model: "account",
1453
1002
  where: [
1454
1003
  { field: "userId", value: existingUser.id },
@@ -1456,7 +1005,7 @@ const sso = (options) => {
1456
1005
  { field: "accountId", value: userInfo.id }
1457
1006
  ]
1458
1007
  });
1459
- if (!account) {
1008
+ if (!accounts) {
1460
1009
  const isTrustedProvider = ctx.context.options.account?.accountLinking?.trustedProviders?.includes(
1461
1010
  provider.providerId
1462
1011
  );
@@ -1546,8 +1095,9 @@ const sso = (options) => {
1546
1095
  }
1547
1096
  let session = await ctx.context.internalAdapter.createSession(user.id, ctx);
1548
1097
  await setSessionCookie(ctx, { session, user });
1549
- const callbackUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
1550
- throw ctx.redirect(callbackUrl);
1098
+ throw ctx.redirect(
1099
+ RelayState || `${parsedSamlConfig.callbackUrl}` || `${parsedSamlConfig.issuer}`
1100
+ );
1551
1101
  }
1552
1102
  )
1553
1103
  },