@better-auth/sso 1.7.0-beta.0 → 1.7.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/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-C22JHwcK.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,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "./version-CzfTSPRz.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-C22JHwcK.mjs";
2
2
  import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
3
3
  import { XMLParser, XMLValidator } from "fast-xml-parser";
4
4
  import * as saml from "samlify";
@@ -693,7 +693,10 @@ async function validateInResponseTo(c, ctx) {
693
693
  * could be accepted.
694
694
  */
695
695
  function validateAudience(c, ctx) {
696
- if (!ctx.expectedAudience) return;
696
+ if (!ctx.expectedAudience) {
697
+ c.context.logger.warn("Could not determine SP entity ID for audience validation; skipping", { providerId: ctx.providerId });
698
+ return;
699
+ }
697
700
  const audience = ctx.extract.audience;
698
701
  if (!audience) {
699
702
  c.context.logger.error("SAML assertion missing AudienceRestriction but audience is configured — rejecting", { providerId: ctx.providerId });
@@ -751,7 +754,6 @@ const oidcConfigSchema = z.object({
751
754
  const samlConfigSchema = z.object({
752
755
  entryPoint: z.string().url().optional(),
753
756
  cert: z.string().optional(),
754
- callbackUrl: z.string().url().optional(),
755
757
  audience: z.string().optional(),
756
758
  idpMetadata: z.object({
757
759
  metadata: z.string().optional(),
@@ -783,8 +785,6 @@ const samlConfigSchema = z.object({
783
785
  digestAlgorithm: z.string().optional(),
784
786
  identifierFormat: z.string().optional(),
785
787
  privateKey: z.string().optional(),
786
- decryptionPvk: z.string().optional(),
787
- additionalParams: z.record(z.string(), z.any()).optional(),
788
788
  mapping: samlMappingSchema
789
789
  });
790
790
  const updateSSOProviderBodySchema = z.object({
@@ -861,7 +861,6 @@ function sanitizeProvider(provider, baseURL) {
861
861
  } : void 0,
862
862
  samlConfig: samlConfig ? {
863
863
  entryPoint: samlConfig.entryPoint,
864
- callbackUrl: samlConfig.callbackUrl,
865
864
  audience: samlConfig.audience,
866
865
  wantAssertionsSigned: samlConfig.wantAssertionsSigned,
867
866
  authnRequestsSigned: samlConfig.authnRequestsSigned,
@@ -964,7 +963,6 @@ function mergeSAMLConfig(current, updates, issuer) {
964
963
  issuer,
965
964
  entryPoint: updates.entryPoint ?? current.entryPoint,
966
965
  cert: updates.cert ?? current.cert,
967
- callbackUrl: updates.callbackUrl ?? current.callbackUrl,
968
966
  spMetadata: updates.spMetadata ?? current.spMetadata,
969
967
  idpMetadata: updates.idpMetadata ?? current.idpMetadata,
970
968
  mapping: updates.mapping ?? current.mapping,
@@ -1510,6 +1508,16 @@ async function parseRelayState(c) {
1510
1508
  }
1511
1509
  //#endregion
1512
1510
  //#region src/routes/helpers.ts
1511
+ /**
1512
+ * Normalizes a PEM string by trimming leading/trailing whitespace from each
1513
+ * line. Native `crypto.createPrivateKey` (used by samlify 2.12+) rejects PEM
1514
+ * blocks with leading whitespace, which is common when keys are stored in
1515
+ * indented config files, environment variables, or JSON.
1516
+ */
1517
+ function normalizePem(pem) {
1518
+ if (!pem) return pem;
1519
+ return pem.split("\n").map((line) => line.trim()).join("\n");
1520
+ }
1513
1521
  async function findSAMLProvider(providerId, options, adapter) {
1514
1522
  if (options?.defaultSSO?.length) {
1515
1523
  const match = options.defaultSSO.find((p) => p.providerId === providerId);
@@ -1536,30 +1544,35 @@ async function findSAMLProvider(providerId, options, adapter) {
1536
1544
  function createSP(config, baseURL, providerId, opts) {
1537
1545
  const spData = config.spMetadata;
1538
1546
  const sloLocation = `${baseURL}/sso/saml2/sp/slo/${providerId}`;
1539
- const acsUrl = config.callbackUrl || `${baseURL}/sso/saml2/sp/acs/${providerId}`;
1540
- return saml.ServiceProvider({
1547
+ const acsUrl = `${baseURL}/sso/saml2/sp/acs/${providerId}`;
1548
+ let metadata = spData?.metadata;
1549
+ if (!metadata) metadata = saml.SPMetadata({
1541
1550
  entityID: spData?.entityID || config.issuer,
1542
- assertionConsumerService: spData?.metadata ? void 0 : [{
1551
+ assertionConsumerService: [{
1543
1552
  Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
1544
1553
  Location: acsUrl
1545
1554
  }],
1546
- singleLogoutService: [{
1555
+ singleLogoutService: opts?.sloOptions ? [{
1547
1556
  Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
1548
1557
  Location: sloLocation
1549
1558
  }, {
1550
1559
  Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1551
1560
  Location: sloLocation
1552
- }],
1561
+ }] : void 0,
1553
1562
  wantMessageSigned: config.wantAssertionsSigned || false,
1563
+ authnRequestsSigned: config.authnRequestsSigned || false,
1564
+ nameIDFormat: config.identifierFormat ? [config.identifierFormat] : void 0
1565
+ }).getMetadata() || "";
1566
+ return saml.ServiceProvider({
1567
+ metadata,
1568
+ allowCreate: true,
1554
1569
  wantLogoutRequestSigned: opts?.sloOptions?.wantLogoutRequestSigned ?? false,
1555
1570
  wantLogoutResponseSigned: opts?.sloOptions?.wantLogoutResponseSigned ?? false,
1556
- metadata: spData?.metadata,
1557
- privateKey: spData?.privateKey || config.privateKey,
1571
+ privateKey: normalizePem(spData?.privateKey || config.privateKey),
1558
1572
  privateKeyPass: spData?.privateKeyPass,
1559
1573
  isAssertionEncrypted: spData?.isAssertionEncrypted || false,
1560
- encPrivateKey: spData?.encPrivateKey,
1574
+ encPrivateKey: normalizePem(spData?.encPrivateKey),
1561
1575
  encPrivateKeyPass: spData?.encPrivateKeyPass,
1562
- nameIDFormat: config.identifierFormat ? [config.identifierFormat] : void 0,
1563
1576
  relayState: opts?.relayState
1564
1577
  });
1565
1578
  }
@@ -1567,10 +1580,10 @@ function createIdP(config) {
1567
1580
  const idpData = config.idpMetadata;
1568
1581
  if (idpData?.metadata) return saml.IdentityProvider({
1569
1582
  metadata: idpData.metadata,
1570
- privateKey: idpData.privateKey,
1583
+ privateKey: normalizePem(idpData.privateKey),
1571
1584
  privateKeyPass: idpData.privateKeyPass,
1572
1585
  isAssertionEncrypted: idpData.isAssertionEncrypted,
1573
- encPrivateKey: idpData.encPrivateKey,
1586
+ encPrivateKey: normalizePem(idpData.encPrivateKey),
1574
1587
  encPrivateKeyPass: idpData.encPrivateKeyPass
1575
1588
  });
1576
1589
  return saml.IdentityProvider({
@@ -1580,10 +1593,10 @@ function createIdP(config) {
1580
1593
  Location: config.entryPoint
1581
1594
  }],
1582
1595
  singleLogoutService: idpData?.singleLogoutService,
1583
- signingCert: idpData?.cert || config.cert,
1596
+ signingCert: normalizePem(idpData?.cert || config.cert),
1584
1597
  wantAuthnRequestsSigned: config.authnRequestsSigned || false,
1585
1598
  isAssertionEncrypted: idpData?.isAssertionEncrypted || false,
1586
- encPrivateKey: idpData?.encPrivateKey,
1599
+ encPrivateKey: normalizePem(idpData?.encPrivateKey),
1587
1600
  encPrivateKeyPass: idpData?.encPrivateKeyPass
1588
1601
  });
1589
1602
  }
@@ -1694,8 +1707,8 @@ function extractAssertionId(samlContent) {
1694
1707
  /**
1695
1708
  * Unified SAML response processing pipeline.
1696
1709
  *
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,
1710
+ * The `/sso/saml2/sp/acs/:providerId` endpoint delegates to this function.
1711
+ * It handles the full lifecycle: provider lookup,
1699
1712
  * SP/IdP construction, response validation, session creation, and redirect
1700
1713
  * URL computation.
1701
1714
  */
@@ -1718,7 +1731,7 @@ async function processSAMLResponse(ctx, params, options) {
1718
1731
  if (!parsedSamlConfig) throw new APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
1719
1732
  const sp = createSP(parsedSamlConfig, ctx.context.baseURL, providerId);
1720
1733
  const idp = createIdP(parsedSamlConfig);
1721
- const samlRedirectUrl = getSafeRedirectUrl(relayState?.callbackURL || parsedSamlConfig.callbackUrl, params.currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
1734
+ const samlRedirectUrl = getSafeRedirectUrl(relayState?.callbackURL, params.currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
1722
1735
  validateSingleAssertion(SAMLResponse);
1723
1736
  let parsedResponse;
1724
1737
  try {
@@ -1755,7 +1768,7 @@ async function processSAMLResponse(ctx, params, options) {
1755
1768
  });
1756
1769
  validateAudience(ctx, {
1757
1770
  extract,
1758
- expectedAudience: parsedSamlConfig.audience,
1771
+ expectedAudience: parsedSamlConfig.audience || sp.entityMeta.getEntityID(),
1759
1772
  providerId,
1760
1773
  redirectUrl: samlRedirectUrl
1761
1774
  });
@@ -1815,7 +1828,7 @@ async function processSAMLResponse(ctx, params, options) {
1815
1828
  throw new APIError("BAD_REQUEST", { message: "Unable to extract user ID or email from SAML response" });
1816
1829
  }
1817
1830
  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;
1831
+ const postAuthRedirect = relayState?.callbackURL || ctx.context.baseURL;
1819
1832
  const result = await handleOAuthUserInfo(ctx, {
1820
1833
  userInfo: {
1821
1834
  email: userInfo.email,
@@ -1829,7 +1842,7 @@ async function processSAMLResponse(ctx, params, options) {
1829
1842
  accessToken: "",
1830
1843
  refreshToken: ""
1831
1844
  },
1832
- callbackURL: callbackUrl,
1845
+ callbackURL: postAuthRedirect,
1833
1846
  disableSignUp: options?.disableImplicitSignUp,
1834
1847
  isTrustedProvider
1835
1848
  });
@@ -1876,7 +1889,7 @@ async function processSAMLResponse(ctx, params, options) {
1876
1889
  expiresAt: session.expiresAt
1877
1890
  }).catch((e) => ctx.context.logger.warn("Failed to create SAML session lookup record", e));
1878
1891
  }
1879
- return getSafeRedirectUrl(relayState?.callbackURL || parsedSamlConfig.callbackUrl, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
1892
+ return getSafeRedirectUrl(relayState?.callbackURL, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
1880
1893
  }
1881
1894
  //#endregion
1882
1895
  //#region src/routes/sso.ts
@@ -1893,10 +1906,7 @@ function getOIDCRedirectURI(baseURL, providerId, options) {
1893
1906
  }
1894
1907
  return `${baseURL}/sso/callback/${providerId}`;
1895
1908
  }
1896
- const spMetadataQuerySchema = z.object({
1897
- providerId: z.string(),
1898
- format: z.enum(["xml", "json"]).default("xml")
1899
- });
1909
+ const spMetadataQuerySchema = z.object({ providerId: z.string() });
1900
1910
  const spMetadata = (options) => {
1901
1911
  return createAuthEndpoint("/sso/saml2/sp/metadata", {
1902
1912
  method: "GET",
@@ -1918,25 +1928,10 @@ const spMetadata = (options) => {
1918
1928
  if (!provider) throw new APIError("NOT_FOUND", { message: "No provider found for the given providerId" });
1919
1929
  const parsedSamlConfig = safeJsonParse(provider.samlConfig);
1920
1930
  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
- });
1931
+ const sp = createSP(parsedSamlConfig, ctx.context.baseURL, ctx.query.providerId, options?.saml?.enableSingleLogout ? { sloOptions: {
1932
+ wantLogoutRequestSigned: options?.saml?.wantLogoutRequestSigned,
1933
+ wantLogoutResponseSigned: options?.saml?.wantLogoutResponseSigned
1934
+ } } : void 0);
1940
1935
  return new Response(sp.getMetadata(), { headers: { "Content-Type": "application/xml" } });
1941
1936
  });
1942
1937
  };
@@ -1974,7 +1969,6 @@ const ssoProviderBodySchema = z.object({
1974
1969
  samlConfig: z.object({
1975
1970
  entryPoint: z.string({}).meta({ description: "The entry point of the provider" }),
1976
1971
  cert: z.string({}).meta({ description: "The certificate of the provider" }),
1977
- callbackUrl: z.string({}).meta({ description: "The callback URL of the provider" }),
1978
1972
  audience: z.string().optional(),
1979
1973
  idpMetadata: z.object({
1980
1974
  metadata: z.string().optional(),
@@ -1999,15 +1993,13 @@ const ssoProviderBodySchema = z.object({
1999
1993
  isAssertionEncrypted: z.boolean().optional(),
2000
1994
  encPrivateKey: z.string().optional(),
2001
1995
  encPrivateKeyPass: z.string().optional()
2002
- }),
1996
+ }).optional(),
2003
1997
  wantAssertionsSigned: z.boolean().optional(),
2004
1998
  authnRequestsSigned: z.boolean().optional(),
2005
1999
  signatureAlgorithm: z.string().optional(),
2006
2000
  digestAlgorithm: z.string().optional(),
2007
2001
  identifierFormat: z.string().optional(),
2008
2002
  privateKey: z.string().optional(),
2009
- decryptionPvk: z.string().optional(),
2010
- additionalParams: z.record(z.string(), z.any()).optional(),
2011
2003
  mapping: z.object({
2012
2004
  id: z.string({}).meta({ description: "Field mapping for user ID (defaults to 'nameID')" }),
2013
2005
  email: z.string({}).meta({ description: "Field mapping for email (defaults to 'email')" }),
@@ -2321,7 +2313,6 @@ const registerSSOProvider = (options) => {
2321
2313
  issuer: body.issuer,
2322
2314
  entryPoint: body.samlConfig.entryPoint,
2323
2315
  cert: body.samlConfig.cert,
2324
- callbackUrl: body.samlConfig.callbackUrl,
2325
2316
  audience: body.samlConfig.audience,
2326
2317
  idpMetadata: body.samlConfig.idpMetadata,
2327
2318
  spMetadata: body.samlConfig.spMetadata,
@@ -2331,8 +2322,6 @@ const registerSSOProvider = (options) => {
2331
2322
  digestAlgorithm: body.samlConfig.digestAlgorithm,
2332
2323
  identifierFormat: body.samlConfig.identifierFormat,
2333
2324
  privateKey: body.samlConfig.privateKey,
2334
- decryptionPvk: body.samlConfig.decryptionPvk,
2335
- additionalParams: body.samlConfig.additionalParams,
2336
2325
  mapping: body.samlConfig.mapping
2337
2326
  }) : null,
2338
2327
  organizationId: body.organizationId,
@@ -2539,46 +2528,8 @@ const signInSSO = (options) => {
2539
2528
  if (!parsedSamlConfig) throw new APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
2540
2529
  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
2530
  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
- });
2531
+ const sp = createSP(parsedSamlConfig, ctx.context.baseURL, provider.providerId, { relayState });
2532
+ const idp = createIdP(parsedSamlConfig);
2582
2533
  const loginRequest = sp.createLoginRequest(idp, "redirect");
2583
2534
  if (!loginRequest) throw new APIError("BAD_REQUEST", { message: "Invalid SAML request" });
2584
2535
  if (loginRequest.id && options?.saml?.enableInResponseToValidation !== false) {
@@ -2853,72 +2804,42 @@ const callbackSSOShared = (options) => {
2853
2804
  return handleOIDCCallback(ctx, options, providerId, stateData);
2854
2805
  });
2855
2806
  };
2856
- const callbackSSOSAMLBodySchema = z.object({
2807
+ const acsEndpointBodySchema = z.object({
2857
2808
  SAMLResponse: z.string(),
2858
2809
  RelayState: z.string().optional()
2859
2810
  });
2860
- const callbackSSOSAML = (options) => {
2861
- return createAuthEndpoint("/sso/saml2/callback/:providerId", {
2811
+ const acsEndpoint = (options) => {
2812
+ return createAuthEndpoint("/sso/saml2/sp/acs/:providerId", {
2862
2813
  method: ["GET", "POST"],
2863
- body: callbackSSOSAMLBodySchema.optional(),
2814
+ body: acsEndpointBodySchema.optional(),
2864
2815
  query: z.object({ RelayState: z.string().optional() }).optional(),
2865
2816
  metadata: {
2866
2817
  ...HIDE_METADATA,
2867
2818
  allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
2868
2819
  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.",
2820
+ operationId: "handleSAMLAssertionConsumerService",
2821
+ summary: "SAML Assertion Consumer Service",
2822
+ description: "Handles SAML responses from IdP after successful authentication. Supports GET for post-auth redirects and POST for SAML response processing.",
2872
2823
  responses: {
2873
- "302": { description: "Redirects to the callback URL" },
2874
- "400": { description: "Invalid SAML response" },
2875
- "401": { description: "Unauthorized - SAML authentication failed" }
2824
+ "302": { description: "Redirects after authentication (success or error with query params)" },
2825
+ "400": { description: "Missing SAMLResponse in POST body" },
2826
+ "404": { description: "SAML provider not found" }
2876
2827
  }
2877
2828
  }
2878
2829
  }
2879
2830
  }, async (ctx) => {
2880
2831
  const { providerId } = ctx.params;
2832
+ const currentCallbackPath = `${ctx.context.baseURL}/sso/saml2/sp/acs/${providerId}`;
2881
2833
  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
2834
  if (ctx.method === "GET" && !ctx.body?.SAMLResponse) {
2885
- if (!(await getSessionFromCtx(ctx))?.session) throw ctx.redirect(`${errorURL}?error=invalid_request`);
2835
+ if (!(await getSessionFromCtx(ctx))?.session) {
2836
+ const errorURL = ctx.context.options.onAPIError?.errorURL || `${appOrigin}/error`;
2837
+ throw ctx.redirect(`${errorURL}?error=invalid_request`);
2838
+ }
2886
2839
  const relayState = ctx.query?.RelayState;
2887
- const safeRedirectUrl = getSafeRedirectUrl(relayState, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
2888
- throw ctx.redirect(safeRedirectUrl);
2840
+ throw ctx.redirect(getSafeRedirectUrl(relayState, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings)));
2889
2841
  }
2890
2842
  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
2843
  try {
2923
2844
  const safeRedirectUrl = await processSAMLResponse(ctx, {
2924
2845
  SAMLResponse: ctx.body.SAMLResponse,
@@ -2930,9 +2851,8 @@ const acsEndpoint = (options) => {
2930
2851
  } catch (error) {
2931
2852
  if (error instanceof Response || error && typeof error === "object" && "status" in error && error.status === 302) throw error;
2932
2853
  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));
2854
+ const errorCode = (error.body?.code || "saml_error").toLowerCase();
2855
+ const redirectUrl = getSafeRedirectUrl(ctx.body?.RelayState || void 0, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
2936
2856
  throw ctx.redirect(`${redirectUrl}${redirectUrl.includes("?") ? "&" : "?"}error=${encodeURIComponent(errorCode)}&error_description=${encodeURIComponent(error.message)}`);
2937
2857
  }
2938
2858
  throw error;
@@ -3107,11 +3027,7 @@ saml.setSchemaValidator({ async validate(xml) {
3107
3027
  * These endpoints receive POST requests from external Identity Providers,
3108
3028
  * which won't have a matching Origin header.
3109
3029
  */
3110
- const SAML_SKIP_ORIGIN_CHECK_PATHS = [
3111
- "/sso/saml2/callback",
3112
- "/sso/saml2/sp/acs",
3113
- "/sso/saml2/sp/slo"
3114
- ];
3030
+ const SAML_SKIP_ORIGIN_CHECK_PATHS = ["/sso/saml2/sp/acs", "/sso/saml2/sp/slo"];
3115
3031
  function sso(options) {
3116
3032
  const optionsWithStore = options;
3117
3033
  let endpoints = {
@@ -3120,7 +3036,6 @@ function sso(options) {
3120
3036
  signInSSO: signInSSO(optionsWithStore),
3121
3037
  callbackSSO: callbackSSO(optionsWithStore),
3122
3038
  callbackSSOShared: callbackSSOShared(optionsWithStore),
3123
- callbackSSOSAML: callbackSSOSAML(optionsWithStore),
3124
3039
  acsEndpoint: acsEndpoint(optionsWithStore),
3125
3040
  sloEndpoint: sloEndpoint(optionsWithStore),
3126
3041
  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.1";
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.1",
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.1",
74
+ "better-auth": "1.7.0-beta.1"
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.1",
81
+ "better-auth": "^1.7.0-beta.1"
82
82
  },
83
83
  "scripts": {
84
84
  "build": "tsdown",