@better-auth/sso 1.7.0-beta.0 → 1.7.0-beta.2

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/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as SSOPlugin } from "./index-DVg_iWRX.mjs";
1
+ import { t as SSOPlugin } from "./index-CagV4mMx.mjs";
2
2
 
3
3
  //#region src/client.d.ts
4
4
  interface SSOClientOptions {
package/dist/client.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "./version-CzfTSPRz.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-BVHIqaH7.mjs";
2
2
  //#region src/client.ts
3
3
  const ssoClient = (options) => {
4
4
  return {
@@ -81,16 +81,29 @@ interface OIDCConfig {
81
81
  mapping?: OIDCMapping | undefined;
82
82
  }
83
83
  interface SAMLConfig {
84
+ /**
85
+ * SP Entity ID. Used as the `entityID` in SP metadata when
86
+ * `spMetadata.entityID` is not set. Also used as the expected
87
+ * audience for SAML assertion validation when `audience` is not set.
88
+ */
84
89
  issuer: string;
90
+ /**
91
+ * IdP SSO URL. Used as the redirect destination when
92
+ * `idpMetadata.metadata` is not provided. Ignored when
93
+ * IdP metadata XML is set (the SSO URL is extracted from the XML).
94
+ */
85
95
  entryPoint: string;
96
+ /**
97
+ * IdP signing certificate. Used to verify SAML response signatures
98
+ * when `idpMetadata.metadata` is not provided. Ignored when IdP
99
+ * metadata XML is set (the certificate is extracted from the XML).
100
+ * When both this and `idpMetadata.cert` are set, `idpMetadata.cert` takes precedence.
101
+ */
86
102
  cert: string;
87
- callbackUrl: string;
88
103
  audience?: string | undefined;
89
104
  idpMetadata?: {
90
105
  metadata?: string;
91
106
  entityID?: string;
92
- entityURL?: string;
93
- redirectURL?: string;
94
107
  cert?: string;
95
108
  privateKey?: string;
96
109
  privateKeyPass?: string;
@@ -106,7 +119,12 @@ interface SAMLConfig {
106
119
  Location: string;
107
120
  }>;
108
121
  } | undefined;
109
- spMetadata: {
122
+ /**
123
+ * SP metadata configuration. All fields are optional; when omitted,
124
+ * SP metadata is auto-generated from `issuer`, `wantAssertionsSigned`,
125
+ * `authnRequestsSigned`, and `identifierFormat`.
126
+ */
127
+ spMetadata?: {
110
128
  metadata?: string | undefined;
111
129
  entityID?: string | undefined;
112
130
  binding?: string | undefined;
@@ -116,14 +134,17 @@ interface SAMLConfig {
116
134
  encPrivateKey?: string | undefined;
117
135
  encPrivateKeyPass?: string | undefined;
118
136
  };
137
+ /**
138
+ * Request signed assertions from the IdP. When true, the SP metadata
139
+ * advertises `WantAssertionsSigned="true"` and samlify will reject
140
+ * unsigned assertions.
141
+ */
119
142
  wantAssertionsSigned?: boolean | undefined;
120
143
  authnRequestsSigned?: boolean | undefined;
121
144
  signatureAlgorithm?: string | undefined;
122
145
  digestAlgorithm?: string | undefined;
123
146
  identifierFormat?: string | undefined;
124
147
  privateKey?: string | undefined;
125
- decryptionPvk?: string | undefined;
126
- additionalParams?: Record<string, any> | undefined;
127
148
  mapping?: SAMLMapping | undefined;
128
149
  }
129
150
  type BaseSSOProvider = {
@@ -614,7 +635,6 @@ declare const listSSOProviders: () => better_call0.StrictEndpoint<"/sso/provider
614
635
  } | undefined;
615
636
  samlConfig: {
616
637
  entryPoint: string;
617
- callbackUrl: string;
618
638
  audience: string | undefined;
619
639
  wantAssertionsSigned: boolean | undefined;
620
640
  authnRequestsSigned: boolean | undefined;
@@ -699,7 +719,6 @@ declare const getSSOProvider: () => better_call0.StrictEndpoint<"/sso/get-provid
699
719
  } | undefined;
700
720
  samlConfig: {
701
721
  entryPoint: string;
702
- callbackUrl: string;
703
722
  audience: string | undefined;
704
723
  wantAssertionsSigned: boolean | undefined;
705
724
  authnRequestsSigned: boolean | undefined;
@@ -775,7 +794,6 @@ declare const updateSSOProvider: (options: SSOOptions) => better_call0.StrictEnd
775
794
  samlConfig: z.ZodOptional<z.ZodObject<{
776
795
  entryPoint: z.ZodOptional<z.ZodString>;
777
796
  cert: z.ZodOptional<z.ZodString>;
778
- callbackUrl: z.ZodOptional<z.ZodString>;
779
797
  audience: z.ZodOptional<z.ZodString>;
780
798
  idpMetadata: z.ZodOptional<z.ZodObject<{
781
799
  metadata: z.ZodOptional<z.ZodString>;
@@ -807,8 +825,6 @@ declare const updateSSOProvider: (options: SSOOptions) => better_call0.StrictEnd
807
825
  digestAlgorithm: z.ZodOptional<z.ZodString>;
808
826
  identifierFormat: z.ZodOptional<z.ZodString>;
809
827
  privateKey: z.ZodOptional<z.ZodString>;
810
- decryptionPvk: z.ZodOptional<z.ZodString>;
811
- additionalParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
812
828
  mapping: z.ZodOptional<z.ZodObject<{
813
829
  id: z.ZodOptional<z.ZodString>;
814
830
  email: z.ZodOptional<z.ZodString>;
@@ -859,7 +875,6 @@ declare const updateSSOProvider: (options: SSOOptions) => better_call0.StrictEnd
859
875
  } | undefined;
860
876
  samlConfig: {
861
877
  entryPoint: string;
862
- callbackUrl: string;
863
878
  audience: string | undefined;
864
879
  wantAssertionsSigned: boolean | undefined;
865
880
  authnRequestsSigned: boolean | undefined;
@@ -932,10 +947,6 @@ declare const spMetadata: (options?: SSOOptions) => better_call0.StrictEndpoint<
932
947
  method: "GET";
933
948
  query: z.ZodObject<{
934
949
  providerId: z.ZodString;
935
- format: z.ZodDefault<z.ZodEnum<{
936
- json: "json";
937
- xml: "xml";
938
- }>>;
939
950
  }, z.core.$strip>;
940
951
  metadata: {
941
952
  openapi: {
@@ -986,7 +997,6 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
986
997
  samlConfig: z.ZodOptional<z.ZodObject<{
987
998
  entryPoint: z.ZodString;
988
999
  cert: z.ZodString;
989
- callbackUrl: z.ZodString;
990
1000
  audience: z.ZodOptional<z.ZodString>;
991
1001
  idpMetadata: z.ZodOptional<z.ZodObject<{
992
1002
  metadata: z.ZodOptional<z.ZodString>;
@@ -1002,7 +1012,7 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
1002
1012
  Location: z.ZodString;
1003
1013
  }, z.core.$strip>>>;
1004
1014
  }, z.core.$strip>>;
1005
- spMetadata: z.ZodObject<{
1015
+ spMetadata: z.ZodOptional<z.ZodObject<{
1006
1016
  metadata: z.ZodOptional<z.ZodString>;
1007
1017
  entityID: z.ZodOptional<z.ZodString>;
1008
1018
  binding: z.ZodOptional<z.ZodString>;
@@ -1011,15 +1021,13 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
1011
1021
  isAssertionEncrypted: z.ZodOptional<z.ZodBoolean>;
1012
1022
  encPrivateKey: z.ZodOptional<z.ZodString>;
1013
1023
  encPrivateKeyPass: z.ZodOptional<z.ZodString>;
1014
- }, z.core.$strip>;
1024
+ }, z.core.$strip>>;
1015
1025
  wantAssertionsSigned: z.ZodOptional<z.ZodBoolean>;
1016
1026
  authnRequestsSigned: z.ZodOptional<z.ZodBoolean>;
1017
1027
  signatureAlgorithm: z.ZodOptional<z.ZodString>;
1018
1028
  digestAlgorithm: z.ZodOptional<z.ZodString>;
1019
1029
  identifierFormat: z.ZodOptional<z.ZodString>;
1020
1030
  privateKey: z.ZodOptional<z.ZodString>;
1021
- decryptionPvk: z.ZodOptional<z.ZodString>;
1022
- additionalParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
1023
1031
  mapping: z.ZodOptional<z.ZodObject<{
1024
1032
  id: z.ZodString;
1025
1033
  email: z.ZodString;
@@ -1376,7 +1384,7 @@ declare const callbackSSOShared: (options?: SSOOptions) => better_call0.StrictEn
1376
1384
  }, z.core.$strip>;
1377
1385
  allowedMediaTypes: readonly ["application/x-www-form-urlencoded", "application/json"];
1378
1386
  }, void>;
1379
- declare const callbackSSOSAML: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/saml2/callback/:providerId", {
1387
+ declare const acsEndpoint: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/saml2/sp/acs/:providerId", {
1380
1388
  method: ("POST" | "GET")[];
1381
1389
  body: z.ZodOptional<z.ZodObject<{
1382
1390
  SAMLResponse: z.ZodString;
@@ -1398,28 +1406,7 @@ declare const callbackSSOSAML: (options?: SSOOptions) => better_call0.StrictEndp
1398
1406
  "400": {
1399
1407
  description: string;
1400
1408
  };
1401
- "401": {
1402
- description: string;
1403
- };
1404
- };
1405
- };
1406
- scope: "server";
1407
- };
1408
- }, never>;
1409
- declare const acsEndpoint: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sso/saml2/sp/acs/:providerId", {
1410
- method: "POST";
1411
- body: z.ZodObject<{
1412
- SAMLResponse: z.ZodString;
1413
- RelayState: z.ZodOptional<z.ZodString>;
1414
- }, z.core.$strip>;
1415
- metadata: {
1416
- allowedMediaTypes: string[];
1417
- openapi: {
1418
- operationId: string;
1419
- summary: string;
1420
- description: string;
1421
- responses: {
1422
- "302": {
1409
+ "404": {
1423
1410
  description: string;
1424
1411
  };
1425
1412
  };
@@ -1788,7 +1775,6 @@ type SSOEndpoints<O extends SSOOptions> = {
1788
1775
  signInSSO: ReturnType<typeof signInSSO>;
1789
1776
  callbackSSO: ReturnType<typeof callbackSSO>;
1790
1777
  callbackSSOShared: ReturnType<typeof callbackSSOShared>;
1791
- callbackSSOSAML: ReturnType<typeof callbackSSOSAML>;
1792
1778
  acsEndpoint: ReturnType<typeof acsEndpoint>;
1793
1779
  sloEndpoint: ReturnType<typeof sloEndpoint>;
1794
1780
  initiateSLO: ReturnType<typeof initiateSLO>;
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as DataEncryptionAlgorithm, C as DEFAULT_MAX_SAML_METADATA_SIZE, D as SSOOptions, E as SAMLConfig, M as DigestAlgorithm, N as KeyEncryptionAlgorithm, O as SSOProvider, P as SignatureAlgorithm, S as DEFAULT_CLOCK_SKEW_MS, T as OIDCConfig, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as TimestampValidationOptions, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as DeprecatedAlgorithmBehavior, k as AlgorithmValidationOptions, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as DEFAULT_MAX_SAML_RESPONSE_SIZE, x as validateSAMLTimestamp, y as SAMLConditions } from "./index-DVg_iWRX.mjs";
1
+ import { A as DataEncryptionAlgorithm, C as DEFAULT_MAX_SAML_METADATA_SIZE, D as SSOOptions, E as SAMLConfig, M as DigestAlgorithm, N as KeyEncryptionAlgorithm, O as SSOProvider, P as SignatureAlgorithm, S as DEFAULT_CLOCK_SKEW_MS, T as OIDCConfig, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as TimestampValidationOptions, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as DeprecatedAlgorithmBehavior, k as AlgorithmValidationOptions, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as DEFAULT_MAX_SAML_RESPONSE_SIZE, x as validateSAMLTimestamp, y as SAMLConditions } from "./index-CagV4mMx.mjs";
2
2
  export { AlgorithmValidationOptions, DEFAULT_CLOCK_SKEW_MS, DEFAULT_MAX_SAML_METADATA_SIZE, DEFAULT_MAX_SAML_RESPONSE_SIZE, DataEncryptionAlgorithm, DeprecatedAlgorithmBehavior, DigestAlgorithm, DiscoverOIDCConfigParams, DiscoveryError, DiscoveryErrorCode, HydratedOIDCConfig, KeyEncryptionAlgorithm, OIDCConfig, OIDCDiscoveryDocument, REQUIRED_DISCOVERY_FIELDS, RequiredDiscoveryField, SAMLConditions, SAMLConfig, SSOOptions, SSOPlugin, SSOProvider, SignatureAlgorithm, TimestampValidationOptions, computeDiscoveryUrl, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
package/dist/index.mjs CHANGED
@@ -1,7 +1,6 @@
1
- import { t as PACKAGE_VERSION } from "./version-CzfTSPRz.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-BVHIqaH7.mjs";
2
2
  import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
3
3
  import { XMLParser, XMLValidator } from "fast-xml-parser";
4
- import * as saml from "samlify";
5
4
  import { X509Certificate } from "node:crypto";
6
5
  import { getHostname } from "tldts";
7
6
  import { generateRandomString } from "better-auth/crypto";
@@ -13,6 +12,8 @@ import { deleteSessionCookie, setSessionCookie } from "better-auth/cookies";
13
12
  import { handleOAuthUserInfo } from "better-auth/oauth2";
14
13
  import { decodeJwt } from "jose";
15
14
  import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
15
+ import * as samlifyNamespace from "samlify";
16
+ import samlifyDefault from "samlify";
16
17
  //#region src/constants.ts
17
18
  /**
18
19
  * SAML Constants
@@ -693,7 +694,10 @@ async function validateInResponseTo(c, ctx) {
693
694
  * could be accepted.
694
695
  */
695
696
  function validateAudience(c, ctx) {
696
- if (!ctx.expectedAudience) return;
697
+ if (!ctx.expectedAudience) {
698
+ c.context.logger.warn("Could not determine SP entity ID for audience validation; skipping", { providerId: ctx.providerId });
699
+ return;
700
+ }
697
701
  const audience = ctx.extract.audience;
698
702
  if (!audience) {
699
703
  c.context.logger.error("SAML assertion missing AudienceRestriction but audience is configured — rejecting", { providerId: ctx.providerId });
@@ -751,7 +755,6 @@ const oidcConfigSchema = z.object({
751
755
  const samlConfigSchema = z.object({
752
756
  entryPoint: z.string().url().optional(),
753
757
  cert: z.string().optional(),
754
- callbackUrl: z.string().url().optional(),
755
758
  audience: z.string().optional(),
756
759
  idpMetadata: z.object({
757
760
  metadata: z.string().optional(),
@@ -783,8 +786,6 @@ const samlConfigSchema = z.object({
783
786
  digestAlgorithm: z.string().optional(),
784
787
  identifierFormat: z.string().optional(),
785
788
  privateKey: z.string().optional(),
786
- decryptionPvk: z.string().optional(),
787
- additionalParams: z.record(z.string(), z.any()).optional(),
788
789
  mapping: samlMappingSchema
789
790
  });
790
791
  const updateSSOProviderBodySchema = z.object({
@@ -861,7 +862,6 @@ function sanitizeProvider(provider, baseURL) {
861
862
  } : void 0,
862
863
  samlConfig: samlConfig ? {
863
864
  entryPoint: samlConfig.entryPoint,
864
- callbackUrl: samlConfig.callbackUrl,
865
865
  audience: samlConfig.audience,
866
866
  wantAssertionsSigned: samlConfig.wantAssertionsSigned,
867
867
  authnRequestsSigned: samlConfig.authnRequestsSigned,
@@ -964,7 +964,6 @@ function mergeSAMLConfig(current, updates, issuer) {
964
964
  issuer,
965
965
  entryPoint: updates.entryPoint ?? current.entryPoint,
966
966
  cert: updates.cert ?? current.cert,
967
- callbackUrl: updates.callbackUrl ?? current.callbackUrl,
968
967
  spMetadata: updates.spMetadata ?? current.spMetadata,
969
968
  idpMetadata: updates.idpMetadata ?? current.idpMetadata,
970
969
  mapping: updates.mapping ?? current.mapping,
@@ -1508,8 +1507,19 @@ async function parseRelayState(c) {
1508
1507
  if (!parsedData.errorURL) parsedData.errorURL = errorURL;
1509
1508
  return parsedData;
1510
1509
  }
1510
+ const saml = typeof samlifyNamespace.SPMetadata === "function" && typeof samlifyNamespace.setSchemaValidator === "function" ? samlifyNamespace : samlifyDefault ?? samlifyNamespace;
1511
1511
  //#endregion
1512
1512
  //#region src/routes/helpers.ts
1513
+ /**
1514
+ * Normalizes a PEM string by trimming leading/trailing whitespace from each
1515
+ * line. Native `crypto.createPrivateKey` (used by samlify 2.12+) rejects PEM
1516
+ * blocks with leading whitespace, which is common when keys are stored in
1517
+ * indented config files, environment variables, or JSON.
1518
+ */
1519
+ function normalizePem(pem) {
1520
+ if (!pem) return pem;
1521
+ return pem.split("\n").map((line) => line.trim()).join("\n");
1522
+ }
1513
1523
  async function findSAMLProvider(providerId, options, adapter) {
1514
1524
  if (options?.defaultSSO?.length) {
1515
1525
  const match = options.defaultSSO.find((p) => p.providerId === providerId);
@@ -1536,30 +1546,35 @@ async function findSAMLProvider(providerId, options, adapter) {
1536
1546
  function createSP(config, baseURL, providerId, opts) {
1537
1547
  const spData = config.spMetadata;
1538
1548
  const sloLocation = `${baseURL}/sso/saml2/sp/slo/${providerId}`;
1539
- const acsUrl = config.callbackUrl || `${baseURL}/sso/saml2/sp/acs/${providerId}`;
1540
- return saml.ServiceProvider({
1549
+ const acsUrl = `${baseURL}/sso/saml2/sp/acs/${providerId}`;
1550
+ let metadata = spData?.metadata;
1551
+ if (!metadata) metadata = saml.SPMetadata({
1541
1552
  entityID: spData?.entityID || config.issuer,
1542
- assertionConsumerService: spData?.metadata ? void 0 : [{
1553
+ assertionConsumerService: [{
1543
1554
  Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
1544
1555
  Location: acsUrl
1545
1556
  }],
1546
- singleLogoutService: [{
1557
+ singleLogoutService: opts?.sloOptions ? [{
1547
1558
  Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
1548
1559
  Location: sloLocation
1549
1560
  }, {
1550
1561
  Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1551
1562
  Location: sloLocation
1552
- }],
1563
+ }] : void 0,
1553
1564
  wantMessageSigned: config.wantAssertionsSigned || false,
1565
+ authnRequestsSigned: config.authnRequestsSigned || false,
1566
+ nameIDFormat: config.identifierFormat ? [config.identifierFormat] : void 0
1567
+ }).getMetadata() || "";
1568
+ return saml.ServiceProvider({
1569
+ metadata,
1570
+ allowCreate: true,
1554
1571
  wantLogoutRequestSigned: opts?.sloOptions?.wantLogoutRequestSigned ?? false,
1555
1572
  wantLogoutResponseSigned: opts?.sloOptions?.wantLogoutResponseSigned ?? false,
1556
- metadata: spData?.metadata,
1557
- privateKey: spData?.privateKey || config.privateKey,
1573
+ privateKey: normalizePem(spData?.privateKey || config.privateKey),
1558
1574
  privateKeyPass: spData?.privateKeyPass,
1559
1575
  isAssertionEncrypted: spData?.isAssertionEncrypted || false,
1560
- encPrivateKey: spData?.encPrivateKey,
1576
+ encPrivateKey: normalizePem(spData?.encPrivateKey),
1561
1577
  encPrivateKeyPass: spData?.encPrivateKeyPass,
1562
- nameIDFormat: config.identifierFormat ? [config.identifierFormat] : void 0,
1563
1578
  relayState: opts?.relayState
1564
1579
  });
1565
1580
  }
@@ -1567,10 +1582,10 @@ function createIdP(config) {
1567
1582
  const idpData = config.idpMetadata;
1568
1583
  if (idpData?.metadata) return saml.IdentityProvider({
1569
1584
  metadata: idpData.metadata,
1570
- privateKey: idpData.privateKey,
1585
+ privateKey: normalizePem(idpData.privateKey),
1571
1586
  privateKeyPass: idpData.privateKeyPass,
1572
1587
  isAssertionEncrypted: idpData.isAssertionEncrypted,
1573
- encPrivateKey: idpData.encPrivateKey,
1588
+ encPrivateKey: normalizePem(idpData.encPrivateKey),
1574
1589
  encPrivateKeyPass: idpData.encPrivateKeyPass
1575
1590
  });
1576
1591
  return saml.IdentityProvider({
@@ -1580,10 +1595,10 @@ function createIdP(config) {
1580
1595
  Location: config.entryPoint
1581
1596
  }],
1582
1597
  singleLogoutService: idpData?.singleLogoutService,
1583
- signingCert: idpData?.cert || config.cert,
1598
+ signingCert: normalizePem(idpData?.cert || config.cert),
1584
1599
  wantAuthnRequestsSigned: config.authnRequestsSigned || false,
1585
1600
  isAssertionEncrypted: idpData?.isAssertionEncrypted || false,
1586
- encPrivateKey: idpData?.encPrivateKey,
1601
+ encPrivateKey: normalizePem(idpData?.encPrivateKey),
1587
1602
  encPrivateKeyPass: idpData?.encPrivateKeyPass
1588
1603
  });
1589
1604
  }
@@ -1694,8 +1709,8 @@ function extractAssertionId(samlContent) {
1694
1709
  /**
1695
1710
  * Unified SAML response processing pipeline.
1696
1711
  *
1697
- * Both `/sso/saml2/callback/:providerId` (POST) and `/sso/saml2/sp/acs/:providerId`
1698
- * delegate to this function. It handles the full lifecycle: provider lookup,
1712
+ * The `/sso/saml2/sp/acs/:providerId` endpoint delegates to this function.
1713
+ * It handles the full lifecycle: provider lookup,
1699
1714
  * SP/IdP construction, response validation, session creation, and redirect
1700
1715
  * URL computation.
1701
1716
  */
@@ -1718,7 +1733,7 @@ async function processSAMLResponse(ctx, params, options) {
1718
1733
  if (!parsedSamlConfig) throw new APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
1719
1734
  const sp = createSP(parsedSamlConfig, ctx.context.baseURL, providerId);
1720
1735
  const idp = createIdP(parsedSamlConfig);
1721
- const samlRedirectUrl = getSafeRedirectUrl(relayState?.callbackURL || parsedSamlConfig.callbackUrl, params.currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
1736
+ const samlRedirectUrl = getSafeRedirectUrl(relayState?.callbackURL, params.currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
1722
1737
  validateSingleAssertion(SAMLResponse);
1723
1738
  let parsedResponse;
1724
1739
  try {
@@ -1755,7 +1770,7 @@ async function processSAMLResponse(ctx, params, options) {
1755
1770
  });
1756
1771
  validateAudience(ctx, {
1757
1772
  extract,
1758
- expectedAudience: parsedSamlConfig.audience,
1773
+ expectedAudience: parsedSamlConfig.audience || sp.entityMeta.getEntityID(),
1759
1774
  providerId,
1760
1775
  redirectUrl: samlRedirectUrl
1761
1776
  });
@@ -1815,7 +1830,7 @@ async function processSAMLResponse(ctx, params, options) {
1815
1830
  throw new APIError("BAD_REQUEST", { message: "Unable to extract user ID or email from SAML response" });
1816
1831
  }
1817
1832
  const isTrustedProvider = ctx.context.trustedProviders.includes(providerId) || "domainVerified" in provider && !!provider.domainVerified && validateEmailDomain(userInfo.email, provider.domain);
1818
- const callbackUrl = relayState?.callbackURL || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
1833
+ const postAuthRedirect = relayState?.callbackURL || ctx.context.baseURL;
1819
1834
  const result = await handleOAuthUserInfo(ctx, {
1820
1835
  userInfo: {
1821
1836
  email: userInfo.email,
@@ -1829,7 +1844,7 @@ async function processSAMLResponse(ctx, params, options) {
1829
1844
  accessToken: "",
1830
1845
  refreshToken: ""
1831
1846
  },
1832
- callbackURL: callbackUrl,
1847
+ callbackURL: postAuthRedirect,
1833
1848
  disableSignUp: options?.disableImplicitSignUp,
1834
1849
  isTrustedProvider
1835
1850
  });
@@ -1876,7 +1891,7 @@ async function processSAMLResponse(ctx, params, options) {
1876
1891
  expiresAt: session.expiresAt
1877
1892
  }).catch((e) => ctx.context.logger.warn("Failed to create SAML session lookup record", e));
1878
1893
  }
1879
- return getSafeRedirectUrl(relayState?.callbackURL || parsedSamlConfig.callbackUrl, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
1894
+ return getSafeRedirectUrl(relayState?.callbackURL, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
1880
1895
  }
1881
1896
  //#endregion
1882
1897
  //#region src/routes/sso.ts
@@ -1893,10 +1908,7 @@ function getOIDCRedirectURI(baseURL, providerId, options) {
1893
1908
  }
1894
1909
  return `${baseURL}/sso/callback/${providerId}`;
1895
1910
  }
1896
- const spMetadataQuerySchema = z.object({
1897
- providerId: z.string(),
1898
- format: z.enum(["xml", "json"]).default("xml")
1899
- });
1911
+ const spMetadataQuerySchema = z.object({ providerId: z.string() });
1900
1912
  const spMetadata = (options) => {
1901
1913
  return createAuthEndpoint("/sso/saml2/sp/metadata", {
1902
1914
  method: "GET",
@@ -1918,25 +1930,10 @@ const spMetadata = (options) => {
1918
1930
  if (!provider) throw new APIError("NOT_FOUND", { message: "No provider found for the given providerId" });
1919
1931
  const parsedSamlConfig = safeJsonParse(provider.samlConfig);
1920
1932
  if (!parsedSamlConfig) throw new APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
1921
- const sloLocation = `${ctx.context.baseURL}/sso/saml2/sp/slo/${ctx.query.providerId}`;
1922
- const singleLogoutService = options?.saml?.enableSingleLogout ? [{
1923
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
1924
- Location: sloLocation
1925
- }, {
1926
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1927
- Location: sloLocation
1928
- }] : void 0;
1929
- const sp = parsedSamlConfig.spMetadata.metadata ? saml.ServiceProvider({ metadata: parsedSamlConfig.spMetadata.metadata }) : saml.SPMetadata({
1930
- entityID: parsedSamlConfig.spMetadata?.entityID || parsedSamlConfig.issuer,
1931
- assertionConsumerService: [{
1932
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
1933
- Location: parsedSamlConfig.callbackUrl || `${ctx.context.baseURL}/sso/saml2/sp/acs/${ctx.query.providerId}`
1934
- }],
1935
- singleLogoutService,
1936
- wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
1937
- authnRequestsSigned: parsedSamlConfig.authnRequestsSigned || false,
1938
- nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
1939
- });
1933
+ const sp = createSP(parsedSamlConfig, ctx.context.baseURL, ctx.query.providerId, options?.saml?.enableSingleLogout ? { sloOptions: {
1934
+ wantLogoutRequestSigned: options?.saml?.wantLogoutRequestSigned,
1935
+ wantLogoutResponseSigned: options?.saml?.wantLogoutResponseSigned
1936
+ } } : void 0);
1940
1937
  return new Response(sp.getMetadata(), { headers: { "Content-Type": "application/xml" } });
1941
1938
  });
1942
1939
  };
@@ -1974,7 +1971,6 @@ const ssoProviderBodySchema = z.object({
1974
1971
  samlConfig: z.object({
1975
1972
  entryPoint: z.string({}).meta({ description: "The entry point of the provider" }),
1976
1973
  cert: z.string({}).meta({ description: "The certificate of the provider" }),
1977
- callbackUrl: z.string({}).meta({ description: "The callback URL of the provider" }),
1978
1974
  audience: z.string().optional(),
1979
1975
  idpMetadata: z.object({
1980
1976
  metadata: z.string().optional(),
@@ -1999,15 +1995,13 @@ const ssoProviderBodySchema = z.object({
1999
1995
  isAssertionEncrypted: z.boolean().optional(),
2000
1996
  encPrivateKey: z.string().optional(),
2001
1997
  encPrivateKeyPass: z.string().optional()
2002
- }),
1998
+ }).optional(),
2003
1999
  wantAssertionsSigned: z.boolean().optional(),
2004
2000
  authnRequestsSigned: z.boolean().optional(),
2005
2001
  signatureAlgorithm: z.string().optional(),
2006
2002
  digestAlgorithm: z.string().optional(),
2007
2003
  identifierFormat: z.string().optional(),
2008
2004
  privateKey: z.string().optional(),
2009
- decryptionPvk: z.string().optional(),
2010
- additionalParams: z.record(z.string(), z.any()).optional(),
2011
2005
  mapping: z.object({
2012
2006
  id: z.string({}).meta({ description: "Field mapping for user ID (defaults to 'nameID')" }),
2013
2007
  email: z.string({}).meta({ description: "Field mapping for email (defaults to 'email')" }),
@@ -2321,7 +2315,6 @@ const registerSSOProvider = (options) => {
2321
2315
  issuer: body.issuer,
2322
2316
  entryPoint: body.samlConfig.entryPoint,
2323
2317
  cert: body.samlConfig.cert,
2324
- callbackUrl: body.samlConfig.callbackUrl,
2325
2318
  audience: body.samlConfig.audience,
2326
2319
  idpMetadata: body.samlConfig.idpMetadata,
2327
2320
  spMetadata: body.samlConfig.spMetadata,
@@ -2331,8 +2324,6 @@ const registerSSOProvider = (options) => {
2331
2324
  digestAlgorithm: body.samlConfig.digestAlgorithm,
2332
2325
  identifierFormat: body.samlConfig.identifierFormat,
2333
2326
  privateKey: body.samlConfig.privateKey,
2334
- decryptionPvk: body.samlConfig.decryptionPvk,
2335
- additionalParams: body.samlConfig.additionalParams,
2336
2327
  mapping: body.samlConfig.mapping
2337
2328
  }) : null,
2338
2329
  organizationId: body.organizationId,
@@ -2539,46 +2530,8 @@ const signInSSO = (options) => {
2539
2530
  if (!parsedSamlConfig) throw new APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
2540
2531
  if (parsedSamlConfig.authnRequestsSigned && !parsedSamlConfig.spMetadata?.privateKey && !parsedSamlConfig.privateKey) throw new APIError("BAD_REQUEST", { message: "authnRequestsSigned is enabled but no privateKey provided in spMetadata or samlConfig" });
2541
2532
  const { state: relayState } = await generateRelayState(ctx, void 0, false);
2542
- let metadata = parsedSamlConfig.spMetadata.metadata;
2543
- if (!metadata) metadata = saml.SPMetadata({
2544
- entityID: parsedSamlConfig.spMetadata?.entityID || parsedSamlConfig.issuer,
2545
- assertionConsumerService: [{
2546
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
2547
- Location: parsedSamlConfig.callbackUrl || `${ctx.context.baseURL}/sso/saml2/sp/acs/${provider.providerId}`
2548
- }],
2549
- wantMessageSigned: parsedSamlConfig.wantAssertionsSigned || false,
2550
- authnRequestsSigned: parsedSamlConfig.authnRequestsSigned || false,
2551
- nameIDFormat: parsedSamlConfig.identifierFormat ? [parsedSamlConfig.identifierFormat] : void 0
2552
- }).getMetadata() || "";
2553
- const sp = saml.ServiceProvider({
2554
- metadata,
2555
- allowCreate: true,
2556
- privateKey: parsedSamlConfig.spMetadata?.privateKey || parsedSamlConfig.privateKey,
2557
- privateKeyPass: parsedSamlConfig.spMetadata?.privateKeyPass,
2558
- relayState
2559
- });
2560
- const idpData = parsedSamlConfig.idpMetadata;
2561
- let idp;
2562
- if (!idpData?.metadata) idp = saml.IdentityProvider({
2563
- entityID: idpData?.entityID || parsedSamlConfig.issuer,
2564
- singleSignOnService: idpData?.singleSignOnService || [{
2565
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
2566
- Location: parsedSamlConfig.entryPoint
2567
- }],
2568
- signingCert: idpData?.cert || parsedSamlConfig.cert,
2569
- wantAuthnRequestsSigned: parsedSamlConfig.authnRequestsSigned || false,
2570
- isAssertionEncrypted: idpData?.isAssertionEncrypted || false,
2571
- encPrivateKey: idpData?.encPrivateKey,
2572
- encPrivateKeyPass: idpData?.encPrivateKeyPass
2573
- });
2574
- else idp = saml.IdentityProvider({
2575
- metadata: idpData.metadata,
2576
- privateKey: idpData.privateKey,
2577
- privateKeyPass: idpData.privateKeyPass,
2578
- isAssertionEncrypted: idpData.isAssertionEncrypted,
2579
- encPrivateKey: idpData.encPrivateKey,
2580
- encPrivateKeyPass: idpData.encPrivateKeyPass
2581
- });
2533
+ const sp = createSP(parsedSamlConfig, ctx.context.baseURL, provider.providerId, { relayState });
2534
+ const idp = createIdP(parsedSamlConfig);
2582
2535
  const loginRequest = sp.createLoginRequest(idp, "redirect");
2583
2536
  if (!loginRequest) throw new APIError("BAD_REQUEST", { message: "Invalid SAML request" });
2584
2537
  if (loginRequest.id && options?.saml?.enableInResponseToValidation !== false) {
@@ -2853,72 +2806,42 @@ const callbackSSOShared = (options) => {
2853
2806
  return handleOIDCCallback(ctx, options, providerId, stateData);
2854
2807
  });
2855
2808
  };
2856
- const callbackSSOSAMLBodySchema = z.object({
2809
+ const acsEndpointBodySchema = z.object({
2857
2810
  SAMLResponse: z.string(),
2858
2811
  RelayState: z.string().optional()
2859
2812
  });
2860
- const callbackSSOSAML = (options) => {
2861
- return createAuthEndpoint("/sso/saml2/callback/:providerId", {
2813
+ const acsEndpoint = (options) => {
2814
+ return createAuthEndpoint("/sso/saml2/sp/acs/:providerId", {
2862
2815
  method: ["GET", "POST"],
2863
- body: callbackSSOSAMLBodySchema.optional(),
2816
+ body: acsEndpointBodySchema.optional(),
2864
2817
  query: z.object({ RelayState: z.string().optional() }).optional(),
2865
2818
  metadata: {
2866
2819
  ...HIDE_METADATA,
2867
2820
  allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
2868
2821
  openapi: {
2869
- operationId: "handleSAMLCallback",
2870
- summary: "Callback URL for SAML provider",
2871
- description: "This endpoint is used as the callback URL for SAML providers. Supports both GET and POST methods for IdP-initiated and SP-initiated flows.",
2822
+ operationId: "handleSAMLAssertionConsumerService",
2823
+ summary: "SAML Assertion Consumer Service",
2824
+ description: "Handles SAML responses from IdP after successful authentication. Supports GET for post-auth redirects and POST for SAML response processing.",
2872
2825
  responses: {
2873
- "302": { description: "Redirects to the callback URL" },
2874
- "400": { description: "Invalid SAML response" },
2875
- "401": { description: "Unauthorized - SAML authentication failed" }
2826
+ "302": { description: "Redirects after authentication (success or error with query params)" },
2827
+ "400": { description: "Missing SAMLResponse in POST body" },
2828
+ "404": { description: "SAML provider not found" }
2876
2829
  }
2877
2830
  }
2878
2831
  }
2879
2832
  }, async (ctx) => {
2880
2833
  const { providerId } = ctx.params;
2834
+ const currentCallbackPath = `${ctx.context.baseURL}/sso/saml2/sp/acs/${providerId}`;
2881
2835
  const appOrigin = new URL(ctx.context.baseURL).origin;
2882
- const errorURL = ctx.context.options.onAPIError?.errorURL || `${appOrigin}/error`;
2883
- const currentCallbackPath = `${ctx.context.baseURL}/sso/saml2/callback/${providerId}`;
2884
2836
  if (ctx.method === "GET" && !ctx.body?.SAMLResponse) {
2885
- if (!(await getSessionFromCtx(ctx))?.session) throw ctx.redirect(`${errorURL}?error=invalid_request`);
2837
+ if (!(await getSessionFromCtx(ctx))?.session) {
2838
+ const errorURL = ctx.context.options.onAPIError?.errorURL || `${appOrigin}/error`;
2839
+ throw ctx.redirect(`${errorURL}?error=invalid_request`);
2840
+ }
2886
2841
  const relayState = ctx.query?.RelayState;
2887
- const safeRedirectUrl = getSafeRedirectUrl(relayState, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
2888
- throw ctx.redirect(safeRedirectUrl);
2842
+ throw ctx.redirect(getSafeRedirectUrl(relayState, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings)));
2889
2843
  }
2890
2844
  if (!ctx.body?.SAMLResponse) throw new APIError("BAD_REQUEST", { message: "SAMLResponse is required for POST requests" });
2891
- const safeRedirectUrl = await processSAMLResponse(ctx, {
2892
- SAMLResponse: ctx.body.SAMLResponse,
2893
- RelayState: ctx.body.RelayState,
2894
- providerId,
2895
- currentCallbackPath
2896
- }, options);
2897
- throw ctx.redirect(safeRedirectUrl);
2898
- });
2899
- };
2900
- const acsEndpointBodySchema = z.object({
2901
- SAMLResponse: z.string(),
2902
- RelayState: z.string().optional()
2903
- });
2904
- const acsEndpoint = (options) => {
2905
- return createAuthEndpoint("/sso/saml2/sp/acs/:providerId", {
2906
- method: "POST",
2907
- body: acsEndpointBodySchema,
2908
- metadata: {
2909
- ...HIDE_METADATA,
2910
- allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
2911
- openapi: {
2912
- operationId: "handleSAMLAssertionConsumerService",
2913
- summary: "SAML Assertion Consumer Service",
2914
- description: "Handles SAML responses from IdP after successful authentication",
2915
- responses: { "302": { description: "Redirects to the callback URL after successful authentication" } }
2916
- }
2917
- }
2918
- }, async (ctx) => {
2919
- const { providerId } = ctx.params;
2920
- const currentCallbackPath = `${ctx.context.baseURL}/sso/saml2/sp/acs/${providerId}`;
2921
- const appOrigin = new URL(ctx.context.baseURL).origin;
2922
2845
  try {
2923
2846
  const safeRedirectUrl = await processSAMLResponse(ctx, {
2924
2847
  SAMLResponse: ctx.body.SAMLResponse,
@@ -2930,9 +2853,8 @@ const acsEndpoint = (options) => {
2930
2853
  } catch (error) {
2931
2854
  if (error instanceof Response || error && typeof error === "object" && "status" in error && error.status === 302) throw error;
2932
2855
  if (error instanceof APIError && error.statusCode === 400) {
2933
- const internalCode = error.body?.code || "";
2934
- const errorCode = internalCode === "SAML_MULTIPLE_ASSERTIONS" ? "multiple_assertions" : internalCode === "SAML_NO_ASSERTION" ? "no_assertion" : internalCode.toLowerCase() || "saml_error";
2935
- const redirectUrl = getSafeRedirectUrl(ctx.body.RelayState || void 0, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
2856
+ const errorCode = (error.body?.code || "saml_error").toLowerCase();
2857
+ const redirectUrl = getSafeRedirectUrl(ctx.body?.RelayState || void 0, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
2936
2858
  throw ctx.redirect(`${redirectUrl}${redirectUrl.includes("?") ? "&" : "?"}error=${encodeURIComponent(errorCode)}&error_description=${encodeURIComponent(error.message)}`);
2937
2859
  }
2938
2860
  throw error;
@@ -3107,11 +3029,7 @@ saml.setSchemaValidator({ async validate(xml) {
3107
3029
  * These endpoints receive POST requests from external Identity Providers,
3108
3030
  * which won't have a matching Origin header.
3109
3031
  */
3110
- const SAML_SKIP_ORIGIN_CHECK_PATHS = [
3111
- "/sso/saml2/callback",
3112
- "/sso/saml2/sp/acs",
3113
- "/sso/saml2/sp/slo"
3114
- ];
3032
+ const SAML_SKIP_ORIGIN_CHECK_PATHS = ["/sso/saml2/sp/acs", "/sso/saml2/sp/slo"];
3115
3033
  function sso(options) {
3116
3034
  const optionsWithStore = options;
3117
3035
  let endpoints = {
@@ -3120,7 +3038,6 @@ function sso(options) {
3120
3038
  signInSSO: signInSSO(optionsWithStore),
3121
3039
  callbackSSO: callbackSSO(optionsWithStore),
3122
3040
  callbackSSOShared: callbackSSOShared(optionsWithStore),
3123
- callbackSSOSAML: callbackSSOSAML(optionsWithStore),
3124
3041
  acsEndpoint: acsEndpoint(optionsWithStore),
3125
3042
  sloEndpoint: sloEndpoint(optionsWithStore),
3126
3043
  initiateSLO: initiateSLO(optionsWithStore),
@@ -1,5 +1,5 @@
1
1
  //#endregion
2
2
  //#region src/version.ts
3
- const PACKAGE_VERSION = "1.7.0-beta.0";
3
+ const PACKAGE_VERSION = "1.7.0-beta.2";
4
4
  //#endregion
5
5
  export { PACKAGE_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/sso",
3
- "version": "1.7.0-beta.0",
3
+ "version": "1.7.0-beta.2",
4
4
  "description": "SSO plugin for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -58,7 +58,7 @@
58
58
  "dependencies": {
59
59
  "fast-xml-parser": "^5.5.7",
60
60
  "jose": "^6.1.3",
61
- "samlify": "~2.10.2",
61
+ "samlify": "~2.12.0",
62
62
  "tldts": "^6.1.0",
63
63
  "zod": "^4.3.6"
64
64
  },
@@ -70,15 +70,15 @@
70
70
  "express": "^5.2.1",
71
71
  "oauth2-mock-server": "^8.2.2",
72
72
  "tsdown": "0.21.1",
73
- "@better-auth/core": "1.7.0-beta.0",
74
- "better-auth": "1.7.0-beta.0"
73
+ "@better-auth/core": "1.7.0-beta.2",
74
+ "better-auth": "1.7.0-beta.2"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "@better-auth/utils": "0.4.0",
78
78
  "@better-fetch/fetch": "1.1.21",
79
79
  "better-call": "1.3.5",
80
- "@better-auth/core": "^1.7.0-beta.0",
81
- "better-auth": "^1.7.0-beta.0"
80
+ "@better-auth/core": "^1.7.0-beta.2",
81
+ "better-auth": "^1.7.0-beta.2"
82
82
  },
83
83
  "scripts": {
84
84
  "build": "tsdown",