@better-auth/sso 1.3.17 → 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: {
@@ -101,57 +91,64 @@ const sso = (options) => {
101
91
  {
102
92
  method: "POST",
103
93
  body: z__namespace.object({
104
- providerId: z__namespace.string({}).describe(
105
- "The ID of the provider. This is used to identify the provider during login and callback"
106
- ),
107
- issuer: z__namespace.string({}).describe("The issuer of the provider"),
108
- domain: z__namespace.string({}).describe(
109
- "The domain of the provider. This is used for email matching"
110
- ),
94
+ providerId: z__namespace.string({}).meta({
95
+ description: "The ID of the provider. This is used to identify the provider during login and callback"
96
+ }),
97
+ issuer: z__namespace.string({}).meta({
98
+ description: "The issuer of the provider"
99
+ }),
100
+ domain: z__namespace.string({}).meta({
101
+ description: "The domain of the provider. This is used for email matching"
102
+ }),
111
103
  oidcConfig: z__namespace.object({
112
- clientId: z__namespace.string({}).describe("The client ID"),
113
- clientSecret: z__namespace.string({}).describe("The client secret"),
114
- authorizationEndpoint: z__namespace.string({}).describe("The authorization endpoint").optional(),
115
- tokenEndpoint: z__namespace.string({}).describe("The token endpoint").optional(),
116
- userInfoEndpoint: z__namespace.string({}).describe("The user info endpoint").optional(),
104
+ clientId: z__namespace.string({}).meta({
105
+ description: "The client ID"
106
+ }),
107
+ clientSecret: z__namespace.string({}).meta({
108
+ description: "The client secret"
109
+ }),
110
+ authorizationEndpoint: z__namespace.string({}).meta({
111
+ description: "The authorization endpoint"
112
+ }).optional(),
113
+ tokenEndpoint: z__namespace.string({}).meta({
114
+ description: "The token endpoint"
115
+ }).optional(),
116
+ userInfoEndpoint: z__namespace.string({}).meta({
117
+ description: "The user info endpoint"
118
+ }).optional(),
117
119
  tokenEndpointAuthentication: z__namespace.enum(["client_secret_post", "client_secret_basic"]).optional(),
118
- jwksEndpoint: z__namespace.string({}).describe("The JWKS endpoint").optional(),
120
+ jwksEndpoint: z__namespace.string({}).meta({
121
+ description: "The JWKS endpoint"
122
+ }).optional(),
119
123
  discoveryEndpoint: z__namespace.string().optional(),
120
- scopes: z__namespace.array(z__namespace.string(), {}).describe("The scopes to request. ").optional(),
121
- pkce: z__namespace.boolean({}).describe("Whether to use PKCE for the authorization flow").default(true).optional(),
122
- mapping: z__namespace.object({
123
- id: z__namespace.string({}).describe("Field mapping for user ID ("),
124
- email: z__namespace.string({}).describe("Field mapping for email ("),
125
- emailVerified: z__namespace.string({}).describe("Field mapping for email verification (").optional(),
126
- name: z__namespace.string({}).describe("Field mapping for name ("),
127
- image: z__namespace.string({}).describe("Field mapping for image (").optional(),
128
- extraFields: z__namespace.record(z__namespace.string(), z__namespace.any()).optional()
129
- }).optional()
124
+ scopes: z__namespace.array(z__namespace.string(), {}).meta({
125
+ description: "The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']"
126
+ }).optional(),
127
+ pkce: z__namespace.boolean({}).meta({
128
+ description: "Whether to use PKCE for the authorization flow"
129
+ }).default(true).optional()
130
130
  }).optional(),
131
131
  samlConfig: z__namespace.object({
132
- entryPoint: z__namespace.string({}).describe("The entry point of the provider"),
133
- cert: z__namespace.string({}).describe("The certificate of the provider"),
134
- callbackUrl: z__namespace.string({}).describe("The callback URL of the provider"),
132
+ entryPoint: z__namespace.string({}).meta({
133
+ description: "The entry point of the provider"
134
+ }),
135
+ cert: z__namespace.string({}).meta({
136
+ description: "The certificate of the provider"
137
+ }),
138
+ callbackUrl: z__namespace.string({}).meta({
139
+ description: "The callback URL of the provider"
140
+ }),
135
141
  audience: z__namespace.string().optional(),
136
142
  idpMetadata: z__namespace.object({
137
- metadata: z__namespace.string().optional(),
138
- entityID: z__namespace.string().optional(),
139
- cert: z__namespace.string().optional(),
143
+ metadata: z__namespace.string(),
140
144
  privateKey: z__namespace.string().optional(),
141
145
  privateKeyPass: z__namespace.string().optional(),
142
146
  isAssertionEncrypted: z__namespace.boolean().optional(),
143
147
  encPrivateKey: z__namespace.string().optional(),
144
- encPrivateKeyPass: z__namespace.string().optional(),
145
- singleSignOnService: z__namespace.array(
146
- z__namespace.object({
147
- Binding: z__namespace.string().describe("The binding type for the SSO service"),
148
- Location: z__namespace.string().describe("The URL for the SSO service")
149
- })
150
- ).optional().describe("Single Sign-On service configuration")
148
+ encPrivateKeyPass: z__namespace.string().optional()
151
149
  }).optional(),
152
150
  spMetadata: z__namespace.object({
153
- metadata: z__namespace.string().optional(),
154
- entityID: z__namespace.string().optional(),
151
+ metadata: z__namespace.string(),
155
152
  binding: z__namespace.string().optional(),
156
153
  privateKey: z__namespace.string().optional(),
157
154
  privateKeyPass: z__namespace.string().optional(),
@@ -165,23 +162,32 @@ const sso = (options) => {
165
162
  identifierFormat: z__namespace.string().optional(),
166
163
  privateKey: z__namespace.string().optional(),
167
164
  decryptionPvk: z__namespace.string().optional(),
168
- additionalParams: z__namespace.record(z__namespace.string(), z__namespace.any()).optional(),
169
- mapping: z__namespace.object({
170
- id: z__namespace.string({}).describe("Field mapping for user ID ("),
171
- email: z__namespace.string({}).describe("Field mapping for email ("),
172
- emailVerified: z__namespace.string({}).describe("Field mapping for email verification").optional(),
173
- name: z__namespace.string({}).describe("Field mapping for name ("),
174
- firstName: z__namespace.string({}).describe("Field mapping for first name (").optional(),
175
- lastName: z__namespace.string({}).describe("Field mapping for last name (").optional(),
176
- extraFields: z__namespace.record(z__namespace.string(), z__namespace.any()).optional()
177
- }).optional()
165
+ additionalParams: z__namespace.record(z__namespace.string(), z__namespace.any()).optional()
178
166
  }).optional(),
179
- organizationId: z__namespace.string({}).describe(
180
- "If organization plugin is enabled, the organization id to link the provider to"
181
- ).optional(),
182
- overrideUserInfo: z__namespace.boolean({}).describe(
183
- "Override user info with the provider info. Defaults to false"
184
- ).default(false).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()
184
+ }).optional(),
185
+ organizationId: z__namespace.string({}).meta({
186
+ description: "If organization plugin is enabled, the organization id to link the provider to"
187
+ }).optional(),
188
+ overrideUserInfo: z__namespace.boolean({}).meta({
189
+ description: "Override user info with the provider info. Defaults to false"
190
+ }).default(false).optional()
185
191
  }),
186
192
  use: [api.sessionMiddleware],
187
193
  metadata: {
@@ -411,7 +417,7 @@ const sso = (options) => {
411
417
  jwksEndpoint: body.oidcConfig.jwksEndpoint,
412
418
  pkce: body.oidcConfig.pkce,
413
419
  discoveryEndpoint: body.oidcConfig.discoveryEndpoint || `${body.issuer}/.well-known/openid-configuration`,
414
- mapping: body.oidcConfig.mapping,
420
+ mapping: body.mapping,
415
421
  scopes: body.oidcConfig.scopes,
416
422
  userInfoEndpoint: body.oidcConfig.userInfoEndpoint,
417
423
  overrideUserInfo: ctx.body.overrideUserInfo || options?.defaultOverrideUserInfo || false
@@ -431,7 +437,7 @@ const sso = (options) => {
431
437
  privateKey: body.samlConfig.privateKey,
432
438
  decryptionPvk: body.samlConfig.decryptionPvk,
433
439
  additionalParams: body.samlConfig.additionalParams,
434
- mapping: body.samlConfig.mapping
440
+ mapping: body.mapping
435
441
  }) : null,
436
442
  organizationId: body.organizationId,
437
443
  userId: ctx.context.session.user.id,
@@ -455,21 +461,33 @@ const sso = (options) => {
455
461
  {
456
462
  method: "POST",
457
463
  body: z__namespace.object({
458
- email: z__namespace.string({}).describe(
459
- "The email address to sign in with. This is used to identify the issuer to sign in with"
460
- ).optional(),
461
- organizationSlug: z__namespace.string({}).describe("The slug of the organization to sign in with").optional(),
462
- providerId: z__namespace.string({}).describe(
463
- "The ID of the provider to sign in with. This can be provided instead of email or issuer"
464
- ).optional(),
465
- domain: z__namespace.string({}).describe("The domain of the provider.").optional(),
466
- callbackURL: z__namespace.string({}).describe("The URL to redirect to after login"),
467
- errorCallbackURL: z__namespace.string({}).describe("The URL to redirect to after login").optional(),
468
- newUserCallbackURL: z__namespace.string({}).describe("The URL to redirect to after login if the user is new").optional(),
469
- scopes: z__namespace.array(z__namespace.string(), {}).describe("Scopes to request from the provider.").optional(),
470
- requestSignUp: z__namespace.boolean({}).describe(
471
- "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider"
472
- ).optional(),
464
+ email: z__namespace.string({}).meta({
465
+ description: "The email address to sign in with. This is used to identify the issuer to sign in with. It's optional if the issuer is provided"
466
+ }).optional(),
467
+ organizationSlug: z__namespace.string({}).meta({
468
+ description: "The slug of the organization to sign in with"
469
+ }).optional(),
470
+ providerId: z__namespace.string({}).meta({
471
+ description: "The ID of the provider to sign in with. This can be provided instead of email or issuer"
472
+ }).optional(),
473
+ domain: z__namespace.string({}).meta({
474
+ description: "The domain of the provider."
475
+ }).optional(),
476
+ callbackURL: z__namespace.string({}).meta({
477
+ description: "The URL to redirect to after login"
478
+ }),
479
+ errorCallbackURL: z__namespace.string({}).meta({
480
+ description: "The URL to redirect to after login"
481
+ }).optional(),
482
+ newUserCallbackURL: z__namespace.string({}).meta({
483
+ description: "The URL to redirect to after login if the user is new"
484
+ }).optional(),
485
+ scopes: z__namespace.array(z__namespace.string(), {}).meta({
486
+ description: "Scopes to request from the provider."
487
+ }).optional(),
488
+ requestSignUp: z__namespace.boolean({}).meta({
489
+ description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider"
490
+ }).optional(),
473
491
  providerType: z__namespace.enum(["oidc", "saml"]).optional()
474
492
  }),
475
493
  metadata: {
@@ -543,7 +561,7 @@ const sso = (options) => {
543
561
  async (ctx) => {
544
562
  const body = ctx.body;
545
563
  let { email, organizationSlug, providerId, domain } = body;
546
- if (!options?.defaultSSO?.length && !email && !organizationSlug && !domain && !providerId) {
564
+ if (!email && !organizationSlug && !domain && !providerId) {
547
565
  throw new api.APIError("BAD_REQUEST", {
548
566
  message: "email, organizationSlug, domain or providerId is required"
549
567
  });
@@ -566,48 +584,23 @@ const sso = (options) => {
566
584
  return res.id;
567
585
  });
568
586
  }
569
- let provider = null;
570
- if (options?.defaultSSO?.length) {
571
- const matchingDefault = providerId ? options.defaultSSO.find(
572
- (defaultProvider) => defaultProvider.providerId === providerId
573
- ) : options.defaultSSO.find(
574
- (defaultProvider) => defaultProvider.domain === domain
575
- );
576
- if (matchingDefault) {
577
- provider = {
578
- issuer: matchingDefault.samlConfig?.issuer || matchingDefault.oidcConfig?.issuer || "",
579
- providerId: matchingDefault.providerId,
580
- userId: "default",
581
- oidcConfig: matchingDefault.oidcConfig,
582
- samlConfig: matchingDefault.samlConfig
583
- };
584
- }
585
- }
586
- if (!providerId && !orgId && !domain) {
587
- throw new api.APIError("BAD_REQUEST", {
588
- message: "providerId, orgId or domain is required"
589
- });
590
- }
591
- if (!provider) {
592
- provider = await ctx.context.adapter.findOne({
593
- model: "ssoProvider",
594
- where: [
595
- {
596
- field: providerId ? "providerId" : orgId ? "organizationId" : "domain",
597
- value: providerId || orgId || domain
598
- }
599
- ]
600
- }).then((res) => {
601
- if (!res) {
602
- 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
603
593
  }
604
- return {
605
- ...res,
606
- oidcConfig: res.oidcConfig ? JSON.parse(res.oidcConfig) : void 0,
607
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
608
- };
609
- });
610
- }
594
+ ]
595
+ }).then((res) => {
596
+ if (!res) {
597
+ return null;
598
+ }
599
+ return {
600
+ ...res,
601
+ oidcConfig: JSON.parse(res.oidcConfig)
602
+ };
603
+ });
611
604
  if (!provider) {
612
605
  throw new api.APIError("NOT_FOUND", {
613
606
  message: "No provider found for the issuer"
@@ -651,16 +644,15 @@ const sso = (options) => {
651
644
  });
652
645
  }
653
646
  if (provider.samlConfig) {
654
- const parsedSamlConfig = typeof provider.samlConfig === "object" ? provider.samlConfig : JSON.parse(provider.samlConfig);
647
+ const parsedSamlConfig = JSON.parse(
648
+ provider.samlConfig
649
+ );
655
650
  const sp = saml__namespace.ServiceProvider({
656
651
  metadata: parsedSamlConfig.spMetadata.metadata,
657
652
  allowCreate: true
658
653
  });
659
654
  const idp = saml__namespace.IdentityProvider({
660
- metadata: parsedSamlConfig.idpMetadata?.metadata,
661
- entityID: parsedSamlConfig.idpMetadata?.entityID,
662
- encryptCert: parsedSamlConfig.idpMetadata?.cert,
663
- singleSignOnService: parsedSamlConfig.idpMetadata?.singleSignOnService
655
+ metadata: parsedSamlConfig.idpMetadata.metadata
664
656
  });
665
657
  const loginRequest = sp.createLoginRequest(
666
658
  idp,
@@ -719,38 +711,23 @@ const sso = (options) => {
719
711
  `${errorURL || callbackURL}?error=${error}&error_description=${error_description}`
720
712
  );
721
713
  }
722
- let provider = null;
723
- if (options?.defaultSSO?.length) {
724
- const matchingDefault = options.defaultSSO.find(
725
- (defaultProvider) => defaultProvider.providerId === ctx.params.providerId
726
- );
727
- if (matchingDefault) {
728
- provider = {
729
- ...matchingDefault,
730
- issuer: matchingDefault.oidcConfig?.issuer || "",
731
- userId: "default"
732
- };
733
- }
734
- }
735
- if (!provider) {
736
- provider = await ctx.context.adapter.findOne({
737
- model: "ssoProvider",
738
- where: [
739
- {
740
- field: "providerId",
741
- value: ctx.params.providerId
742
- }
743
- ]
744
- }).then((res) => {
745
- if (!res) {
746
- return null;
714
+ const provider = await ctx.context.adapter.findOne({
715
+ model: "ssoProvider",
716
+ where: [
717
+ {
718
+ field: "providerId",
719
+ value: ctx.params.providerId
747
720
  }
748
- return {
749
- ...res,
750
- oidcConfig: JSON.parse(res.oidcConfig)
751
- };
752
- });
753
- }
721
+ ]
722
+ }).then((res) => {
723
+ if (!res) {
724
+ return null;
725
+ }
726
+ return {
727
+ ...res,
728
+ oidcConfig: JSON.parse(res.oidcConfig)
729
+ };
730
+ });
754
731
  if (!provider) {
755
732
  throw ctx.redirect(
756
733
  `${errorURL || callbackURL}/error?error=invalid_provider&error_description=provider not found`
@@ -974,31 +951,10 @@ const sso = (options) => {
974
951
  async (ctx) => {
975
952
  const { SAMLResponse, RelayState } = ctx.body;
976
953
  const { providerId } = ctx.params;
977
- let provider = null;
978
- if (options?.defaultSSO?.length) {
979
- const matchingDefault = options.defaultSSO.find(
980
- (defaultProvider) => defaultProvider.providerId === providerId
981
- );
982
- if (matchingDefault) {
983
- provider = {
984
- ...matchingDefault,
985
- userId: "default",
986
- issuer: matchingDefault.samlConfig?.issuer || ""
987
- };
988
- }
989
- }
990
- if (!provider) {
991
- provider = await ctx.context.adapter.findOne({
992
- model: "ssoProvider",
993
- where: [{ field: "providerId", value: providerId }]
994
- }).then((res) => {
995
- if (!res) return null;
996
- return {
997
- ...res,
998
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
999
- };
1000
- });
1001
- }
954
+ const provider = await ctx.context.adapter.findOne({
955
+ model: "ssoProvider",
956
+ where: [{ field: "providerId", value: providerId }]
957
+ });
1002
958
  if (!provider) {
1003
959
  throw new api.APIError("NOT_FOUND", {
1004
960
  message: "No provider found for the given providerId"
@@ -1007,389 +963,46 @@ const sso = (options) => {
1007
963
  const parsedSamlConfig = JSON.parse(
1008
964
  provider.samlConfig
1009
965
  );
1010
- const idpData = parsedSamlConfig.idpMetadata;
1011
- let idp = null;
1012
- if (!idpData?.metadata) {
1013
- idp = saml__namespace.IdentityProvider({
1014
- entityID: idpData.entityID || parsedSamlConfig.issuer,
1015
- singleSignOnService: [
1016
- {
1017
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1018
- Location: parsedSamlConfig.entryPoint
1019
- }
1020
- ],
1021
- signingCert: idpData.cert || parsedSamlConfig.cert,
1022
- wantAuthnRequestsSigned: parsedSamlConfig.wantAssertionsSigned || false,
1023
- isAssertionEncrypted: idpData.isAssertionEncrypted || false,
1024
- encPrivateKey: idpData.encPrivateKey,
1025
- encPrivateKeyPass: idpData.encPrivateKeyPass
1026
- });
1027
- } else {
1028
- idp = saml__namespace.IdentityProvider({
1029
- metadata: idpData.metadata,
1030
- privateKey: idpData.privateKey,
1031
- privateKeyPass: idpData.privateKeyPass,
1032
- isAssertionEncrypted: idpData.isAssertionEncrypted,
1033
- encPrivateKey: idpData.encPrivateKey,
1034
- encPrivateKeyPass: idpData.encPrivateKeyPass
1035
- });
1036
- }
1037
- const spData = parsedSamlConfig.spMetadata;
1038
- const sp = saml__namespace.ServiceProvider({
1039
- metadata: spData?.metadata,
1040
- entityID: spData?.entityID || parsedSamlConfig.issuer,
1041
- assertionConsumerService: spData?.metadata ? void 0 : [
1042
- {
1043
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
1044
- Location: parsedSamlConfig.callbackUrl
1045
- }
1046
- ],
1047
- privateKey: spData?.privateKey || parsedSamlConfig.privateKey,
1048
- privateKeyPass: spData?.privateKeyPass,
1049
- isAssertionEncrypted: spData?.isAssertionEncrypted || false,
1050
- encPrivateKey: spData?.encPrivateKey,
1051
- encPrivateKeyPass: spData?.encPrivateKeyPass,
1052
- wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
1053
- nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
966
+ const idp = saml__namespace.IdentityProvider({
967
+ metadata: parsedSamlConfig.idpMetadata.metadata
1054
968
  });
1055
- let parsedResponse;
1056
- try {
1057
- const decodedResponse = Buffer.from(
1058
- SAMLResponse,
1059
- "base64"
1060
- ).toString("utf-8");
1061
- try {
1062
- parsedResponse = await sp.parseLoginResponse(idp, "post", {
1063
- body: {
1064
- SAMLResponse,
1065
- RelayState: RelayState || void 0
1066
- }
1067
- });
1068
- } catch (parseError) {
1069
- const nameIDMatch = decodedResponse.match(
1070
- /<saml2:NameID[^>]*>([^<]+)<\/saml2:NameID>/
1071
- );
1072
- if (!nameIDMatch) throw parseError;
1073
- parsedResponse = {
1074
- extract: {
1075
- nameID: nameIDMatch[1],
1076
- attributes: { nameID: nameIDMatch[1] },
1077
- sessionIndex: {},
1078
- conditions: {}
1079
- }
1080
- };
1081
- }
1082
- if (!parsedResponse?.extract) {
1083
- throw new Error("Invalid SAML response structure");
1084
- }
1085
- } catch (error) {
1086
- ctx.context.logger.error("SAML response validation failed", {
1087
- error,
1088
- decodedResponse: Buffer.from(SAMLResponse, "base64").toString(
1089
- "utf-8"
1090
- )
1091
- });
1092
- throw new api.APIError("BAD_REQUEST", {
1093
- message: "Invalid SAML response",
1094
- details: error instanceof Error ? error.message : String(error)
1095
- });
1096
- }
1097
- const { extract } = parsedResponse;
1098
- const attributes = extract.attributes || {};
1099
- const mapping = parsedSamlConfig.mapping ?? {};
1100
- const userInfo = {
1101
- ...Object.fromEntries(
1102
- Object.entries(mapping.extraFields || {}).map(([key, value]) => [
1103
- key,
1104
- attributes[value]
1105
- ])
1106
- ),
1107
- id: attributes[mapping.id || "nameID"] || extract.nameID,
1108
- email: attributes[mapping.email || "email"] || extract.nameID,
1109
- name: [
1110
- attributes[mapping.firstName || "givenName"],
1111
- attributes[mapping.lastName || "surname"]
1112
- ].filter(Boolean).join(" ") || attributes[mapping.name || "displayName"] || extract.nameID,
1113
- emailVerified: options?.trustEmailVerified && mapping.emailVerified ? attributes[mapping.emailVerified] || false : false
1114
- };
1115
- if (!userInfo.id || !userInfo.email) {
1116
- ctx.context.logger.error(
1117
- "Missing essential user info from SAML response",
1118
- {
1119
- attributes: Object.keys(attributes),
1120
- mapping,
1121
- extractedId: userInfo.id,
1122
- extractedEmail: userInfo.email
1123
- }
1124
- );
1125
- throw new api.APIError("BAD_REQUEST", {
1126
- message: "Unable to extract user ID or email from SAML response"
1127
- });
1128
- }
1129
- let user;
1130
- const existingUser = await ctx.context.adapter.findOne({
1131
- model: "user",
1132
- where: [
1133
- {
1134
- field: "email",
1135
- value: userInfo.email
1136
- }
1137
- ]
1138
- });
1139
- if (existingUser) {
1140
- user = existingUser;
1141
- } else {
1142
- user = await ctx.context.adapter.create({
1143
- model: "user",
1144
- data: {
1145
- email: userInfo.email,
1146
- name: userInfo.name,
1147
- emailVerified: userInfo.emailVerified,
1148
- createdAt: /* @__PURE__ */ new Date(),
1149
- updatedAt: /* @__PURE__ */ new Date()
1150
- }
1151
- });
1152
- }
1153
- const account = await ctx.context.adapter.findOne({
1154
- model: "account",
1155
- where: [
1156
- { field: "userId", value: user.id },
1157
- { field: "providerId", value: provider.providerId },
1158
- { field: "accountId", value: userInfo.id }
1159
- ]
1160
- });
1161
- if (!account) {
1162
- await ctx.context.adapter.create({
1163
- model: "account",
1164
- data: {
1165
- userId: user.id,
1166
- providerId: provider.providerId,
1167
- accountId: userInfo.id,
1168
- createdAt: /* @__PURE__ */ new Date(),
1169
- updatedAt: /* @__PURE__ */ new Date(),
1170
- accessToken: "",
1171
- refreshToken: ""
1172
- }
1173
- });
1174
- }
1175
- if (options?.provisionUser) {
1176
- await options.provisionUser({
1177
- user,
1178
- userInfo,
1179
- provider
1180
- });
1181
- }
1182
- if (provider.organizationId && !options?.organizationProvisioning?.disabled) {
1183
- const isOrgPluginEnabled = ctx.context.options.plugins?.find(
1184
- (plugin) => plugin.id === "organization"
1185
- );
1186
- if (isOrgPluginEnabled) {
1187
- const isAlreadyMember = await ctx.context.adapter.findOne({
1188
- model: "member",
1189
- where: [
1190
- { field: "organizationId", value: provider.organizationId },
1191
- { field: "userId", value: user.id }
1192
- ]
1193
- });
1194
- if (!isAlreadyMember) {
1195
- const role = options?.organizationProvisioning?.getRole ? await options.organizationProvisioning.getRole({
1196
- user,
1197
- userInfo,
1198
- provider
1199
- }) : options?.organizationProvisioning?.defaultRole || "member";
1200
- await ctx.context.adapter.create({
1201
- model: "member",
1202
- data: {
1203
- organizationId: provider.organizationId,
1204
- userId: user.id,
1205
- role,
1206
- createdAt: /* @__PURE__ */ new Date(),
1207
- updatedAt: /* @__PURE__ */ new Date()
1208
- }
1209
- });
1210
- }
1211
- }
1212
- }
1213
- let session = await ctx.context.internalAdapter.createSession(user.id, ctx);
1214
- await cookies.setSessionCookie(ctx, { session, user });
1215
- const callbackUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
1216
- throw ctx.redirect(callbackUrl);
1217
- }
1218
- ),
1219
- acsEndpoint: plugins.createAuthEndpoint(
1220
- "/sso/saml2/sp/acs/:providerId",
1221
- {
1222
- method: "POST",
1223
- params: z__namespace.object({
1224
- providerId: z__namespace.string().optional()
1225
- }),
1226
- body: z__namespace.object({
1227
- SAMLResponse: z__namespace.string(),
1228
- RelayState: z__namespace.string().optional()
1229
- }),
1230
- metadata: {
1231
- isAction: false,
1232
- openapi: {
1233
- summary: "SAML Assertion Consumer Service",
1234
- description: "Handles SAML responses from IdP after successful authentication",
1235
- responses: {
1236
- "302": {
1237
- description: "Redirects to the callback URL after successful authentication"
1238
- }
1239
- }
1240
- }
1241
- }
1242
- },
1243
- async (ctx) => {
1244
- const { SAMLResponse, RelayState = "" } = ctx.body;
1245
- const { providerId } = ctx.params;
1246
- let provider = null;
1247
- if (options?.defaultSSO?.length) {
1248
- const matchingDefault = providerId ? options.defaultSSO.find(
1249
- (defaultProvider) => defaultProvider.providerId === providerId
1250
- ) : options.defaultSSO[0];
1251
- if (matchingDefault) {
1252
- provider = {
1253
- issuer: matchingDefault.samlConfig?.issuer || "",
1254
- providerId: matchingDefault.providerId,
1255
- userId: "default",
1256
- samlConfig: matchingDefault.samlConfig
1257
- };
1258
- }
1259
- } else {
1260
- provider = await ctx.context.adapter.findOne({
1261
- model: "ssoProvider",
1262
- where: [
1263
- {
1264
- field: "providerId",
1265
- value: providerId ?? "sso"
1266
- }
1267
- ]
1268
- }).then((res) => {
1269
- if (!res) return null;
1270
- return {
1271
- ...res,
1272
- samlConfig: res.samlConfig ? JSON.parse(res.samlConfig) : void 0
1273
- };
1274
- });
1275
- }
1276
- if (!provider?.samlConfig) {
1277
- throw new api.APIError("NOT_FOUND", {
1278
- message: "No SAML provider found"
1279
- });
1280
- }
1281
- const parsedSamlConfig = provider.samlConfig;
1282
969
  const sp = saml__namespace.ServiceProvider({
1283
- entityID: parsedSamlConfig.spMetadata?.entityID || parsedSamlConfig.issuer,
1284
- assertionConsumerService: [
1285
- {
1286
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
1287
- Location: parsedSamlConfig.callbackUrl || `${ctx.context.baseURL}/sso/saml2/sp/acs/${providerId}`
1288
- }
1289
- ],
1290
- wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
1291
- metadata: parsedSamlConfig.spMetadata?.metadata,
1292
- privateKey: parsedSamlConfig.spMetadata?.privateKey || parsedSamlConfig.privateKey,
1293
- privateKeyPass: parsedSamlConfig.spMetadata?.privateKeyPass,
1294
- nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
1295
- });
1296
- const idpData = parsedSamlConfig.idpMetadata;
1297
- const idp = !idpData?.metadata ? saml__namespace.IdentityProvider({
1298
- entityID: idpData?.entityID || parsedSamlConfig.issuer,
1299
- singleSignOnService: idpData?.singleSignOnService || [
1300
- {
1301
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1302
- Location: parsedSamlConfig.entryPoint
1303
- }
1304
- ],
1305
- signingCert: idpData?.cert || parsedSamlConfig.cert
1306
- }) : saml__namespace.IdentityProvider({
1307
- metadata: idpData.metadata
970
+ metadata: parsedSamlConfig.spMetadata.metadata
1308
971
  });
1309
972
  let parsedResponse;
1310
973
  try {
1311
- let decodedResponse = Buffer.from(SAMLResponse, "base64").toString(
1312
- "utf-8"
1313
- );
1314
- if (!decodedResponse.includes("StatusCode")) {
1315
- const insertPoint = decodedResponse.indexOf("</saml2:Issuer>");
1316
- if (insertPoint !== -1) {
1317
- 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);
1318
- }
1319
- } else if (!decodedResponse.includes("saml2:Success")) {
1320
- decodedResponse = decodedResponse.replace(
1321
- /<saml2:StatusCode Value="[^"]+"/,
1322
- '<saml2:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"'
1323
- );
1324
- }
1325
- try {
1326
- parsedResponse = await sp.parseLoginResponse(idp, "post", {
1327
- body: {
1328
- SAMLResponse,
1329
- RelayState: RelayState || void 0
1330
- }
1331
- });
1332
- } catch (parseError) {
1333
- const nameIDMatch = decodedResponse.match(
1334
- /<saml2:NameID[^>]*>([^<]+)<\/saml2:NameID>/
1335
- );
1336
- if (!nameIDMatch) throw parseError;
1337
- parsedResponse = {
1338
- extract: {
1339
- nameID: nameIDMatch[1],
1340
- attributes: { nameID: nameIDMatch[1] },
1341
- sessionIndex: {},
1342
- conditions: {}
1343
- }
1344
- };
1345
- }
1346
- if (!parsedResponse?.extract) {
1347
- 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");
1348
979
  }
1349
980
  } catch (error) {
1350
- ctx.context.logger.error("SAML response validation failed", {
1351
- error,
1352
- decodedResponse: Buffer.from(SAMLResponse, "base64").toString(
1353
- "utf-8"
1354
- )
1355
- });
981
+ ctx.context.logger.error("SAML response validation failed", error);
1356
982
  throw new api.APIError("BAD_REQUEST", {
1357
983
  message: "Invalid SAML response",
1358
984
  details: error instanceof Error ? error.message : String(error)
1359
985
  });
1360
986
  }
1361
987
  const { extract } = parsedResponse;
1362
- const attributes = extract.attributes || {};
1363
- const mapping = parsedSamlConfig.mapping ?? {};
988
+ const attributes = parsedResponse.extract.attributes;
989
+ const mapping = parsedSamlConfig?.mapping ?? {};
1364
990
  const userInfo = {
1365
991
  ...Object.fromEntries(
1366
992
  Object.entries(mapping.extraFields || {}).map(([key, value]) => [
1367
993
  key,
1368
- attributes[value]
994
+ extract.attributes[value]
1369
995
  ])
1370
996
  ),
1371
- id: attributes[mapping.id || "nameID"] || extract.nameID,
1372
- email: attributes[mapping.email || "email"] || extract.nameID,
997
+ id: attributes[mapping.id] || attributes["nameID"],
998
+ email: attributes[mapping.email] || attributes["nameID"] || attributes["email"],
1373
999
  name: [
1374
- attributes[mapping.firstName || "givenName"],
1375
- attributes[mapping.lastName || "surname"]
1376
- ].filter(Boolean).join(" ") || attributes[mapping.name || "displayName"] || extract.nameID,
1377
- 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
1378
1005
  };
1379
- if (!userInfo.id || !userInfo.email) {
1380
- ctx.context.logger.error(
1381
- "Missing essential user info from SAML response",
1382
- {
1383
- attributes: Object.keys(attributes),
1384
- mapping,
1385
- extractedId: userInfo.id,
1386
- extractedEmail: userInfo.email
1387
- }
1388
- );
1389
- throw new api.APIError("BAD_REQUEST", {
1390
- message: "Unable to extract user ID or email from SAML response"
1391
- });
1392
- }
1393
1006
  let user;
1394
1007
  const existingUser = await ctx.context.adapter.findOne({
1395
1008
  model: "user",
@@ -1401,7 +1014,7 @@ const sso = (options) => {
1401
1014
  ]
1402
1015
  });
1403
1016
  if (existingUser) {
1404
- const account = await ctx.context.adapter.findOne({
1017
+ const accounts = await ctx.context.adapter.findOne({
1405
1018
  model: "account",
1406
1019
  where: [
1407
1020
  { field: "userId", value: existingUser.id },
@@ -1409,7 +1022,7 @@ const sso = (options) => {
1409
1022
  { field: "accountId", value: userInfo.id }
1410
1023
  ]
1411
1024
  });
1412
- if (!account) {
1025
+ if (!accounts) {
1413
1026
  const isTrustedProvider = ctx.context.options.account?.accountLinking?.trustedProviders?.includes(
1414
1027
  provider.providerId
1415
1028
  );
@@ -1499,8 +1112,9 @@ const sso = (options) => {
1499
1112
  }
1500
1113
  let session = await ctx.context.internalAdapter.createSession(user.id, ctx);
1501
1114
  await cookies.setSessionCookie(ctx, { session, user });
1502
- const callbackUrl = RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
1503
- throw ctx.redirect(callbackUrl);
1115
+ throw ctx.redirect(
1116
+ RelayState || `${parsedSamlConfig.callbackUrl}` || `${parsedSamlConfig.issuer}`
1117
+ );
1504
1118
  }
1505
1119
  )
1506
1120
  },