@better-auth/sso 1.7.0-rc.2 → 1.7.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "./version-m-Zfjx6T.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-xoFrTW0m.mjs";
2
2
  import { APIError, addOAuthServerContext, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
3
3
  import { XMLParser, XMLValidator } from "fast-xml-parser";
4
4
  import { X509Certificate } from "node:crypto";
@@ -11,15 +11,16 @@ import { classifyHost, isPublicRoutableHost } from "@better-auth/core/utils/host
11
11
  import { betterFetch } from "@better-fetch/fetch";
12
12
  import { createRemoteJWKSet, customFetch, jwtVerify } from "jose";
13
13
  import { base64, base64Url } from "@better-auth/utils/base64";
14
- import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
14
+ import { createHash } from "@better-auth/utils/hash";
15
+ import { DOMParser } from "@xmldom/xmldom";
15
16
  import * as samlifyNamespace from "samlify";
16
17
  import samlifyDefault from "samlify";
18
+ import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
17
19
  import { parseInputData, toZodSchema } from "better-auth/db";
18
20
  import { isAPIError } from "@better-auth/core/utils/is-api-error";
19
21
  import { HIDE_METADATA, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, authorizationCodeRequest, createAuthorizationURL, createPrivateKeyJwtClientAssertionGetter, generateGenericState, generateState, getOAuth2Tokens, parseGenericState, parseState } from "better-auth";
20
22
  import { deleteSessionCookie, setAccountCookie, setSessionCookie } from "better-auth/cookies";
21
23
  import { additionalAuthorizationParamsSchema, handleOAuthUserInfo } from "better-auth/oauth2";
22
- import { createHash } from "@better-auth/utils/hash";
23
24
  //#region src/constants.ts
24
25
  /**
25
26
  * SAML Constants
@@ -817,6 +818,102 @@ function mapDiscoveryErrorToAPIError(error) {
817
818
  }
818
819
  }
819
820
  //#endregion
821
+ //#region src/provider-reference.ts
822
+ const SSO_PROVIDER_STATE_KEY = "ssoProviderReference";
823
+ function serializeCanonical(value) {
824
+ if (value === null) return "null";
825
+ if (Array.isArray(value)) return `[${value.map((entry) => entry === void 0 ? "null" : serializeCanonical(entry)).join(",")}]`;
826
+ switch (typeof value) {
827
+ case "boolean":
828
+ case "number":
829
+ case "string": return JSON.stringify(value);
830
+ case "object": return `{${Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => `${JSON.stringify(key)}:${serializeCanonical(entry)}`).join(",")}}`;
831
+ default: throw new TypeError("SSO provider configuration must be JSON-serializable");
832
+ }
833
+ }
834
+ function withoutOIDCSecret(configuration) {
835
+ if (!configuration) return void 0;
836
+ const result = { ...configuration };
837
+ result.clientSecret = void 0;
838
+ return result;
839
+ }
840
+ function withoutSAMLPrivateKeys(configuration) {
841
+ if (!configuration) return void 0;
842
+ return {
843
+ ...configuration,
844
+ privateKey: void 0,
845
+ idpMetadata: {
846
+ ...configuration.idpMetadata,
847
+ privateKey: void 0,
848
+ privateKeyPass: void 0,
849
+ encPrivateKey: void 0,
850
+ encPrivateKeyPass: void 0
851
+ },
852
+ spMetadata: configuration.spMetadata ? {
853
+ ...configuration.spMetadata,
854
+ privateKey: void 0,
855
+ privateKeyPass: void 0,
856
+ encPrivateKey: void 0,
857
+ encPrivateKeyPass: void 0
858
+ } : void 0
859
+ };
860
+ }
861
+ function getProviderSource(provider) {
862
+ return typeof provider.id === "string" && provider.id.length > 0 ? {
863
+ type: "persisted",
864
+ recordId: provider.id
865
+ } : { type: "configured" };
866
+ }
867
+ async function computeProviderAuthenticationFingerprint(provider) {
868
+ const digest = await createHash("SHA-256").digest(serializeCanonical({
869
+ domain: provider.domain,
870
+ domainVerified: "domainVerified" in provider ? provider.domainVerified : void 0,
871
+ issuer: provider.issuer,
872
+ organizationId: provider.organizationId,
873
+ oidcConfig: withoutOIDCSecret(provider.oidcConfig),
874
+ samlConfig: withoutSAMLPrivateKeys(provider.samlConfig)
875
+ }));
876
+ return base64Url.encode(new Uint8Array(digest), { padding: false });
877
+ }
878
+ async function computeSSOProviderReference(provider) {
879
+ return {
880
+ providerId: provider.providerId,
881
+ source: getProviderSource(provider),
882
+ authenticationConfigurationFingerprint: await computeProviderAuthenticationFingerprint(provider)
883
+ };
884
+ }
885
+ function isRecord$2(value) {
886
+ return typeof value === "object" && value !== null && !Array.isArray(value);
887
+ }
888
+ function parseSSOProviderReference(value) {
889
+ if (!isRecord$2(value) || !isRecord$2(value.source)) return null;
890
+ const providerId = value.providerId;
891
+ const authenticationConfigurationFingerprint = value.authenticationConfigurationFingerprint;
892
+ const source = value.source;
893
+ if (typeof providerId !== "string" || providerId.length === 0 || typeof authenticationConfigurationFingerprint !== "string" || authenticationConfigurationFingerprint.length === 0) return null;
894
+ if (source.type === "configured") return {
895
+ providerId,
896
+ source: { type: "configured" },
897
+ authenticationConfigurationFingerprint
898
+ };
899
+ if (source.type === "persisted" && typeof source.recordId === "string" && source.recordId.length > 0) return {
900
+ providerId,
901
+ source: {
902
+ type: "persisted",
903
+ recordId: source.recordId
904
+ },
905
+ authenticationConfigurationFingerprint
906
+ };
907
+ return null;
908
+ }
909
+ async function isCurrentSSOProviderReference(provider, reference) {
910
+ if (!reference || reference.providerId !== provider.providerId) return false;
911
+ const source = getProviderSource(provider);
912
+ if (source.type !== reference.source.type) return false;
913
+ if (source.type === "persisted" && (reference.source.type !== "persisted" || source.recordId !== reference.source.recordId)) return false;
914
+ return reference.authenticationConfigurationFingerprint === await computeProviderAuthenticationFingerprint(provider);
915
+ }
916
+ //#endregion
820
917
  //#region src/saml/parser.ts
821
918
  const xmlParser = new XMLParser({
822
919
  ignoreAttributes: false,
@@ -1030,8 +1127,12 @@ function validateConfigAlgorithms(config, options = {}) {
1030
1127
  });
1031
1128
  }
1032
1129
  }
1130
+ const saml = typeof samlifyNamespace.SPMetadata === "function" && typeof samlifyNamespace.setSchemaValidator === "function" ? samlifyNamespace : samlifyDefault ?? samlifyNamespace;
1033
1131
  //#endregion
1034
1132
  //#region src/saml/assertions.ts
1133
+ const SAML_ASSERTION_NAMESPACE = "urn:oasis:names:tc:SAML:2.0:assertion";
1134
+ const SAML_PROTOCOL_NAMESPACE = "urn:oasis:names:tc:SAML:2.0:protocol";
1135
+ const XML_SIGNATURE_NAMESPACE = "http://www.w3.org/2000/09/xmldsig#";
1035
1136
  function countAssertions(xml) {
1036
1137
  let parsed;
1037
1138
  try {
@@ -1071,6 +1172,39 @@ function validateSingleAssertion(samlResponse) {
1071
1172
  code: "SAML_MULTIPLE_ASSERTIONS"
1072
1173
  });
1073
1174
  }
1175
+ function assertionSignatureFailure() {
1176
+ return new APIError("BAD_REQUEST", {
1177
+ code: "SAML_ASSERTION_SIGNATURE_REQUIRED",
1178
+ message: "SAML assertion signature is required"
1179
+ });
1180
+ }
1181
+ /**
1182
+ * Verifies that the sole plaintext SAML Assertion, rather than only its
1183
+ * enclosing Response, is authenticated by an XML signature from the IdP.
1184
+ */
1185
+ function verifySAMLAssertionSignature(samlContent, verificationOptions) {
1186
+ try {
1187
+ const document = new DOMParser({ onError: (_level, message) => {
1188
+ throw new Error(message);
1189
+ } }).parseFromString(samlContent, "text/xml");
1190
+ const response = document.documentElement;
1191
+ if (!response || response.localName !== "Response" || response.namespaceURI !== SAML_PROTOCOL_NAMESPACE) throw assertionSignatureFailure();
1192
+ const assertionElements = Array.from(response.childNodes).filter((node) => node.nodeType === 1 && node.localName === "Assertion" && node.namespaceURI === SAML_ASSERTION_NAMESPACE);
1193
+ const assertion = assertionElements.at(0);
1194
+ if (assertionElements.length !== 1 || !assertion) throw assertionSignatureFailure();
1195
+ if (Array.from(assertion.childNodes).filter((node) => node.nodeType === 1 && node.localName === "Signature" && node.namespaceURI === XML_SIGNATURE_NAMESPACE).length !== 1) throw assertionSignatureFailure();
1196
+ for (const node of Array.from(response.childNodes)) if (node.nodeType === 1 && node.localName === "Signature") response.removeChild(node);
1197
+ const assertionId = assertion.getAttribute("ID");
1198
+ if (!assertionId) throw assertionSignatureFailure();
1199
+ const [verified, authenticatedAssertion] = saml.SamlLib.verifySignature(document.toString(), verificationOptions);
1200
+ if (!verified || !authenticatedAssertion) throw assertionSignatureFailure();
1201
+ const authenticatedRoot = new DOMParser().parseFromString(authenticatedAssertion, "text/xml").documentElement;
1202
+ if (!authenticatedRoot || authenticatedRoot.localName !== "Assertion" || authenticatedRoot.namespaceURI !== SAML_ASSERTION_NAMESPACE || authenticatedRoot.getAttribute("ID") !== assertionId) throw assertionSignatureFailure();
1203
+ } catch (error) {
1204
+ if (error instanceof APIError && error.body?.code === "SAML_ASSERTION_SIGNATURE_REQUIRED") throw error;
1205
+ throw assertionSignatureFailure();
1206
+ }
1207
+ }
1074
1208
  //#endregion
1075
1209
  //#region src/saml/error-codes.ts
1076
1210
  const SAML_ERROR_CODES = defineErrorCodes({
@@ -1116,6 +1250,7 @@ function validateCertSources(config) {
1116
1250
  //#region src/saml/response-binding.ts
1117
1251
  const SAML_HTTP_POST_BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
1118
1252
  const SAML_BEARER_CONFIRMATION_METHOD = "urn:oasis:names:tc:SAML:2.0:cm:bearer";
1253
+ const SAML_METADATA_NAMESPACE = "urn:oasis:names:tc:SAML:2.0:metadata";
1119
1254
  function toNode(value) {
1120
1255
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1121
1256
  return value;
@@ -1147,11 +1282,75 @@ function parseSAMLContent(samlContent) {
1147
1282
  code: "SAML_RESPONSE_INVALID_XML"
1148
1283
  });
1149
1284
  }
1285
+ function directMetadataChildren(element, localName) {
1286
+ return Array.from(element.childNodes).filter((node) => node.nodeType === 1 && node.localName === localName && node.namespaceURI === SAML_METADATA_NAMESPACE);
1287
+ }
1288
+ function parseXMLSchemaBoolean(value) {
1289
+ if (value === null) return false;
1290
+ switch (value.trim()) {
1291
+ case "true":
1292
+ case "1": return true;
1293
+ case "false":
1294
+ case "0": return false;
1295
+ default: throw new Error("Invalid XML Schema boolean");
1296
+ }
1297
+ }
1298
+ function isAbsoluteHttpUrl(value) {
1299
+ if (value.includes("#")) return false;
1300
+ try {
1301
+ const url = new URL(value);
1302
+ return url.protocol === "http:" || url.protocol === "https:";
1303
+ } catch {
1304
+ return false;
1305
+ }
1306
+ }
1307
+ /**
1308
+ * Parses the security- and identity-relevant parts of SAML service-provider
1309
+ * metadata without relying on local-name-only XML matching.
1310
+ */
1311
+ function parseSAMLServiceProviderMetadata(metadata) {
1312
+ const document = new DOMParser({ onError: (_level, message) => {
1313
+ throw new Error(message);
1314
+ } }).parseFromString(metadata, "text/xml");
1315
+ const entityDescriptor = document.documentElement;
1316
+ if (!entityDescriptor || entityDescriptor.localName !== "EntityDescriptor" || entityDescriptor.namespaceURI !== SAML_METADATA_NAMESPACE) throw new Error("Invalid SAML EntityDescriptor");
1317
+ const entityID = entityDescriptor.getAttribute("entityID")?.trim();
1318
+ if (!entityID) throw new Error("Missing SAML entityID");
1319
+ const serviceProviderDescriptors = directMetadataChildren(entityDescriptor, "SPSSODescriptor");
1320
+ if (serviceProviderDescriptors.length === 0) throw new Error("SAML metadata must contain an SPSSODescriptor");
1321
+ const acceptedServiceProviderDescriptors = new Set(serviceProviderDescriptors);
1322
+ for (const element of Array.from(document.getElementsByTagName("*"))) {
1323
+ if (element.localName === "EntityDescriptor" && element !== entityDescriptor) throw new Error("Invalid nested SAML EntityDescriptor");
1324
+ if (element.localName === "SPSSODescriptor" && !acceptedServiceProviderDescriptors.has(element)) throw new Error("Invalid SAML SPSSODescriptor namespace or position");
1325
+ if ((element.localName === "AssertionConsumerService" || element.localName === "NameIDFormat") && (element.namespaceURI !== SAML_METADATA_NAMESPACE || !element.parentNode || !acceptedServiceProviderDescriptors.has(element.parentNode))) throw new Error(`Invalid SAML ${element.localName} namespace or position`);
1326
+ }
1327
+ const postAssertionConsumerServiceUrls = [];
1328
+ const nameIDFormats = [];
1329
+ let wantAssertionsSigned = false;
1330
+ for (const descriptor of serviceProviderDescriptors) {
1331
+ wantAssertionsSigned = parseXMLSchemaBoolean(descriptor.getAttribute("WantAssertionsSigned")) || wantAssertionsSigned;
1332
+ for (const nameIDFormat of directMetadataChildren(descriptor, "NameIDFormat")) {
1333
+ const value = nameIDFormat.textContent?.trim();
1334
+ if (value) nameIDFormats.push(value);
1335
+ }
1336
+ for (const service of directMetadataChildren(descriptor, "AssertionConsumerService")) {
1337
+ if (service.getAttribute("Binding") !== "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST") continue;
1338
+ const location = service.getAttribute("Location")?.trim();
1339
+ if (!location || !isAbsoluteHttpUrl(location)) throw new Error("Invalid SAML POST AssertionConsumerService");
1340
+ postAssertionConsumerServiceUrls.push(location);
1341
+ }
1342
+ }
1343
+ return {
1344
+ entityID,
1345
+ nameIDFormats: [...new Set(nameIDFormats)],
1346
+ postAssertionConsumerServiceUrls: [...new Set(postAssertionConsumerServiceUrls)],
1347
+ wantAssertionsSigned
1348
+ };
1349
+ }
1150
1350
  function getSAMLPostAssertionConsumerServiceUrls(metadata) {
1151
1351
  if (!metadata) return [];
1152
1352
  try {
1153
- const locations = toNodeArray(findNode(toNode(xmlParser.parse(metadata)), "SPSSODescriptor")).flatMap((descriptor) => toNodeArray(descriptor.AssertionConsumerService).filter((service) => service["@_Binding"] === SAML_HTTP_POST_BINDING).map((service) => service["@_Location"]).filter((location) => typeof location === "string" && !!location));
1154
- return [...new Set(locations)];
1353
+ return parseSAMLServiceProviderMetadata(metadata).postAssertionConsumerServiceUrls;
1155
1354
  } catch {
1156
1355
  return [];
1157
1356
  }
@@ -1251,7 +1450,7 @@ function errorRedirectUrl(base, error, description) {
1251
1450
  * samlify's parsed output (not at the top level).
1252
1451
  */
1253
1452
  async function validateInResponseTo(c, ctx) {
1254
- if (ctx.options.enableInResponseToValidation === false) return;
1453
+ if (ctx.options.enableInResponseToValidation === false) return null;
1255
1454
  const inResponseTo = ctx.extract.response?.inResponseTo;
1256
1455
  const allowIdpInitiated = ctx.options.allowIdpInitiated ?? false;
1257
1456
  if (inResponseTo) {
@@ -1277,10 +1476,23 @@ async function validateInResponseTo(c, ctx) {
1277
1476
  });
1278
1477
  throw c.redirect(errorRedirectUrl(ctx.redirectUrl, "invalid_saml_response", "Provider mismatch"));
1279
1478
  }
1479
+ const providerReference = parseSSOProviderReference(storedRequest.providerReference);
1480
+ if (!providerReference) {
1481
+ c.context.logger.error("SAML InResponseTo validation failed: provider reference missing or invalid", {
1482
+ inResponseTo,
1483
+ providerId: ctx.providerId
1484
+ });
1485
+ throw c.redirect(errorRedirectUrl(ctx.redirectUrl, "invalid_state", "SSO provider reference missing or invalid"));
1486
+ }
1487
+ return {
1488
+ ...storedRequest,
1489
+ providerReference
1490
+ };
1280
1491
  } else if (!allowIdpInitiated) {
1281
1492
  c.context.logger.error("SAML IdP-initiated SSO rejected: InResponseTo missing and allowIdpInitiated is false", { providerId: ctx.providerId });
1282
1493
  throw c.redirect(errorRedirectUrl(ctx.redirectUrl, "unsolicited_response", "IdP-initiated SSO not allowed"));
1283
1494
  }
1495
+ return null;
1284
1496
  }
1285
1497
  /**
1286
1498
  * Validates the AudienceRestriction of a SAML assertion.
@@ -1310,7 +1522,93 @@ function validateAudience(c, ctx) {
1310
1522
  throw c.redirect(errorRedirectUrl(ctx.redirectUrl, "invalid_saml_response", "Audience mismatch"));
1311
1523
  }
1312
1524
  }
1313
- const saml = typeof samlifyNamespace.SPMetadata === "function" && typeof samlifyNamespace.setSchemaValidator === "function" ? samlifyNamespace : samlifyDefault ?? samlifyNamespace;
1525
+ //#endregion
1526
+ //#region src/user-resolution.ts
1527
+ const SSO_AUTHENTICATION_FAILURE = Symbol("SSO_AUTHENTICATION_FAILURE");
1528
+ function isRecord$1(value) {
1529
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1530
+ }
1531
+ function isNonEmptyString(value) {
1532
+ return typeof value === "string" && value.trim().length > 0;
1533
+ }
1534
+ function isSSOUserResolution(value) {
1535
+ if (!isRecord$1(value)) return false;
1536
+ if (value.action === "continue") return true;
1537
+ if (value.action === "link") return isNonEmptyString(value.userId) && (value.profile === "preserve" || value.profile === "update");
1538
+ return value.action === "reject" && isNonEmptyString(value.code) && (value.message === void 0 || typeof value.message === "string");
1539
+ }
1540
+ function logFailure(logger, message) {
1541
+ try {
1542
+ logger.error(message);
1543
+ } catch {}
1544
+ }
1545
+ function resolutionFailure() {
1546
+ return new APIError("INTERNAL_SERVER_ERROR", {
1547
+ code: "SSO_USER_RESOLUTION_FAILED",
1548
+ message: "Unable to resolve the SSO user"
1549
+ });
1550
+ }
1551
+ async function resolveSSOUser(resolveUser, input, database, logger) {
1552
+ let resolution;
1553
+ try {
1554
+ resolution = await resolveUser(input, { database });
1555
+ } catch {
1556
+ logFailure(logger, "SSO user resolution failed");
1557
+ throw resolutionFailure();
1558
+ }
1559
+ if (!isSSOUserResolution(resolution)) {
1560
+ logFailure(logger, "SSO user resolver returned an invalid decision");
1561
+ throw resolutionFailure();
1562
+ }
1563
+ return resolution;
1564
+ }
1565
+ function assertSSOUserResolutionNativeTransactionSupport(adapter) {
1566
+ assertSSONativeTransactionSupport(adapter, {
1567
+ code: "SSO_USER_RESOLUTION_REQUIRES_NATIVE_TRANSACTIONS",
1568
+ message: "SSO user resolution requires a database adapter with native transaction support"
1569
+ });
1570
+ }
1571
+ function assertSSONativeTransactionSupport(adapter, error) {
1572
+ if (typeof adapter.options?.adapterConfig.transaction === "function") return;
1573
+ throw new APIError("NOT_IMPLEMENTED", {
1574
+ code: error.code,
1575
+ message: error.message
1576
+ });
1577
+ }
1578
+ async function assertSSOUserResolutionAsyncContextSupport(getStorage = getCurrentDBAdapterAsyncLocalStorage) {
1579
+ await assertSSOAsyncContextSupport({
1580
+ code: "SSO_USER_RESOLUTION_REQUIRES_ASYNC_CONTEXT",
1581
+ message: "SSO user resolution requires database transaction async context support"
1582
+ }, getStorage);
1583
+ }
1584
+ async function assertSSOAsyncContextSupport(error, getStorage = getCurrentDBAdapterAsyncLocalStorage) {
1585
+ try {
1586
+ await getStorage();
1587
+ } catch {
1588
+ throw new APIError("NOT_IMPLEMENTED", {
1589
+ code: error.code,
1590
+ message: error.message
1591
+ });
1592
+ }
1593
+ }
1594
+ function assertSSOUserResolutionSessionStorage(options) {
1595
+ if (!options.secondaryStorage || options.session?.storeSessionInDatabase === true && options.session.preserveSessionInDatabase !== true) return;
1596
+ throw new APIError("NOT_IMPLEMENTED", {
1597
+ code: "SSO_USER_RESOLUTION_REQUIRES_DATABASE_SESSIONS",
1598
+ message: "SSO user resolution requires database-backed sessions with database fallback"
1599
+ });
1600
+ }
1601
+ function requireSuccessfulSSOAuthentication(result) {
1602
+ if (!result.error) return result;
1603
+ throw Object.assign(/* @__PURE__ */ new Error("SSO authentication failed"), {
1604
+ [SSO_AUTHENTICATION_FAILURE]: true,
1605
+ result
1606
+ });
1607
+ }
1608
+ function getFailedSSOAuthenticationResult(error) {
1609
+ if (!(error instanceof Error) || !(SSO_AUTHENTICATION_FAILURE in error) || error[SSO_AUTHENTICATION_FAILURE] !== true || !("result" in error)) return;
1610
+ return error.result;
1611
+ }
1314
1612
  //#endregion
1315
1613
  //#region src/routes/helpers.ts
1316
1614
  /**
@@ -1349,6 +1647,7 @@ function createSP(config, baseURL, providerId, opts) {
1349
1647
  const sloLocation = `${baseURL}/sso/saml2/sp/slo/${providerId}`;
1350
1648
  const acsUrl = `${baseURL}/sso/saml2/sp/acs/${providerId}`;
1351
1649
  let metadata = spData?.metadata;
1650
+ assertSAMLServiceProviderMetadataPolicy(config);
1352
1651
  if (!metadata) metadata = saml.SPMetadata({
1353
1652
  entityID: spData?.entityID || config.issuer,
1354
1653
  assertionConsumerService: [{
@@ -1362,7 +1661,7 @@ function createSP(config, baseURL, providerId, opts) {
1362
1661
  Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1363
1662
  Location: sloLocation
1364
1663
  }] : void 0,
1365
- wantMessageSigned: config.wantAssertionsSigned || false,
1664
+ wantAssertionsSigned: config.wantAssertionsSigned || false,
1366
1665
  authnRequestsSigned: config.authnRequestsSigned || false,
1367
1666
  nameIDFormat: config.identifierFormat ? [config.identifierFormat] : void 0
1368
1667
  }).getMetadata() || "";
@@ -1380,6 +1679,42 @@ function createSP(config, baseURL, providerId, opts) {
1380
1679
  clockDrifts: opts?.clockSkew && opts?.clockSkew !== 0 ? [-opts.clockSkew, opts.clockSkew] : void 0
1381
1680
  });
1382
1681
  }
1682
+ /**
1683
+ * Ensures custom SP metadata cannot weaken the configured assertion-signing
1684
+ * policy. This is safe to call before persisting a provider configuration.
1685
+ */
1686
+ function assertSAMLServiceProviderMetadataPolicy(config) {
1687
+ const policy = deriveSAMLServiceProviderPolicy(config);
1688
+ if (config.wantAssertionsSigned !== true || policy.wantAssertionsSigned) return;
1689
+ throw new APIError("BAD_REQUEST", {
1690
+ code: "SAML_SP_METADATA_ASSERTION_SIGNATURE_MISMATCH",
1691
+ message: "SAML service provider metadata must require signed assertions"
1692
+ });
1693
+ }
1694
+ function assertSAMLMetadataSize(metadata, kind, maxMetadataSize) {
1695
+ if (metadata && new TextEncoder().encode(metadata).length > maxMetadataSize) throw new APIError("BAD_REQUEST", { message: `${kind} metadata exceeds maximum allowed size (${maxMetadataSize} bytes)` });
1696
+ }
1697
+ /**
1698
+ * Parses custom SP metadata and returns its effective verification policy.
1699
+ *
1700
+ * Configurations without custom metadata use the code-defined policy directly.
1701
+ * Invalid or unusable custom metadata throws an API error with the
1702
+ * `SAML_INVALID_SP_METADATA` code.
1703
+ */
1704
+ function deriveSAMLServiceProviderPolicy(config) {
1705
+ const metadata = config.spMetadata?.metadata;
1706
+ if (!metadata) return { wantAssertionsSigned: config.wantAssertionsSigned === true };
1707
+ try {
1708
+ const parsedMetadata = parseSAMLServiceProviderMetadata(metadata);
1709
+ if (!parsedMetadata.postAssertionConsumerServiceUrls.length) throw new Error("Unusable SAML service provider metadata");
1710
+ return { wantAssertionsSigned: parsedMetadata.wantAssertionsSigned };
1711
+ } catch {
1712
+ throw new APIError("BAD_REQUEST", {
1713
+ code: "SAML_INVALID_SP_METADATA",
1714
+ message: "Invalid SAML service provider metadata"
1715
+ });
1716
+ }
1717
+ }
1383
1718
  function assertSAMLIdentityProviderAuthority(config) {
1384
1719
  if (config.idpMetadata?.metadata || config.idpMetadata?.entityID) return;
1385
1720
  throw new APIError("BAD_REQUEST", { message: "SAML manual IdP configuration requires idpMetadata.entityID; issuer identifies the service provider and cannot identify the IdP" });
@@ -1409,6 +1744,13 @@ function createIdP(config) {
1409
1744
  encPrivateKeyPass: idpData.encPrivateKeyPass
1410
1745
  });
1411
1746
  }
1747
+ /**
1748
+ * Derive the verified SAML identity-provider entity ID using the same metadata
1749
+ * parsing and manual-configuration validation as SAML authentication.
1750
+ */
1751
+ function deriveSAMLIdentityProviderEntityID(config) {
1752
+ return createIdP(config).entityMeta.getEntityID();
1753
+ }
1412
1754
  function escapeHtml(str) {
1413
1755
  if (!str) return "";
1414
1756
  return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -1586,24 +1928,13 @@ const OIDC_IDENTITY_BOUNDARY_FIELDS = [
1586
1928
  "tokenEndpoint",
1587
1929
  "userInfoEndpoint"
1588
1930
  ];
1589
- const SAML_IDENTITY_BOUNDARY_FIELDS = [
1590
- "audience",
1591
- "callbackUrl",
1592
- "entryPoint",
1593
- "identifierFormat"
1594
- ];
1595
- const SAML_IDP_BOUNDARY_FIELDS = [
1596
- "metadata",
1597
- "entityID",
1598
- "singleSignOnService"
1599
- ];
1600
- const SAML_SP_BOUNDARY_FIELDS = ["metadata", "entityID"];
1601
- function isRecord$2(value) {
1931
+ const SAML_IDENTITY_BOUNDARY_FIELDS = ["audience", "callbackUrl"];
1932
+ function isRecord(value) {
1602
1933
  return typeof value === "object" && value !== null && !Array.isArray(value);
1603
1934
  }
1604
1935
  function stableStringify(value) {
1605
1936
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
1606
- if (isRecord$2(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
1937
+ if (isRecord(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
1607
1938
  return JSON.stringify(value) ?? String(value);
1608
1939
  }
1609
1940
  function identityValueChanged(current, updated) {
@@ -1616,7 +1947,34 @@ function oidcIdentityBoundaryChanged(current, updated) {
1616
1947
  return hasChangedField(current, updated, OIDC_IDENTITY_BOUNDARY_FIELDS);
1617
1948
  }
1618
1949
  function samlIdentityBoundaryChanged(current, updated) {
1619
- return hasChangedField(current, updated, SAML_IDENTITY_BOUNDARY_FIELDS) || hasChangedField(current.idpMetadata, updated.idpMetadata, SAML_IDP_BOUNDARY_FIELDS) || hasChangedField(current.spMetadata, updated.spMetadata, SAML_SP_BOUNDARY_FIELDS);
1950
+ const currentIdentityProvider = createIdP(current).entityMeta;
1951
+ const updatedIdentityProvider = createIdP(updated).entityMeta;
1952
+ let currentServiceProvider;
1953
+ let updatedServiceProvider;
1954
+ try {
1955
+ currentServiceProvider = current.spMetadata?.metadata ? parseSAMLServiceProviderMetadata(current.spMetadata.metadata) : null;
1956
+ updatedServiceProvider = updated.spMetadata?.metadata ? parseSAMLServiceProviderMetadata(updated.spMetadata.metadata) : null;
1957
+ } catch {
1958
+ return true;
1959
+ }
1960
+ const effectiveIdentityBoundary = (config) => {
1961
+ const identityProvider = config === current ? currentIdentityProvider : updatedIdentityProvider;
1962
+ const serviceProvider = config === current ? currentServiceProvider : updatedServiceProvider;
1963
+ return {
1964
+ idpEntityId: identityProvider.getEntityID(),
1965
+ idpRedirectService: identityProvider.getSingleSignOnService("redirect"),
1966
+ idpPostService: identityProvider.getSingleSignOnService("post"),
1967
+ spEntityId: serviceProvider?.entityID ?? config.spMetadata?.entityID ?? config.issuer,
1968
+ spNameIDFormat: serviceProvider?.nameIDFormats.at(0) ?? config.identifierFormat,
1969
+ spPostServices: serviceProvider ? getSAMLPostAssertionConsumerServiceUrls(config.spMetadata?.metadata) : config.callbackUrl ? [config.callbackUrl] : [],
1970
+ wantAssertionsSigned: serviceProvider?.wantAssertionsSigned ?? config.wantAssertionsSigned === true
1971
+ };
1972
+ };
1973
+ const trustAnchors = (identityProvider) => {
1974
+ const certificates = identityProvider.getX509Certificate("signing");
1975
+ return (Array.isArray(certificates) ? certificates : certificates ? [certificates] : []).map((certificate) => normalizePem(certificate) ?? certificate).sort();
1976
+ };
1977
+ return hasChangedField(current, updated, SAML_IDENTITY_BOUNDARY_FIELDS) || identityValueChanged(trustAnchors(currentIdentityProvider), trustAnchors(updatedIdentityProvider)) || identityValueChanged(effectiveIdentityBoundary(current), effectiveIdentityBoundary(updated));
1620
1978
  }
1621
1979
  function parseConfigSnapshot(config, configType) {
1622
1980
  if (!config) return;
@@ -1632,19 +1990,67 @@ function ssoProviderIdentityBoundaryChanged(current, updated) {
1632
1990
  const updatedOidcConfig = parseConfigSnapshot(updated.oidcConfig, "OIDC");
1633
1991
  return Boolean(currentOidcConfig && (!updatedOidcConfig || oidcIdentityBoundaryChanged(currentOidcConfig, updatedOidcConfig)));
1634
1992
  }
1635
- async function lockSSOProviderRow(adapter, providerId) {
1636
- return (await getCurrentAdapter(adapter)).update({
1993
+ async function lockSSOProviderRow(context, provider) {
1994
+ const where = [...provider.id ? [{
1995
+ field: "id",
1996
+ value: provider.id
1997
+ }] : [], {
1998
+ field: "providerId",
1999
+ value: provider.providerId
2000
+ }];
2001
+ return (await getCurrentAdapter(context.adapter)).update({
1637
2002
  model: "ssoProvider",
1638
- where: [{
1639
- field: "providerId",
1640
- value: providerId
1641
- }],
1642
- update: { providerId }
2003
+ where,
2004
+ update: { providerId: provider.providerId }
2005
+ });
2006
+ }
2007
+ async function guardSSOProviderMutation(options, mutation, provider, database, logger) {
2008
+ if (!options?.guardProviderMutation) return;
2009
+ const oidcConfig = provider.oidcConfig ? parseConfigSnapshot(provider.oidcConfig, "OIDC") : void 0;
2010
+ const samlConfig = provider.samlConfig ? parseConfigSnapshot(provider.samlConfig, "SAML") : void 0;
2011
+ const providerReference = await computeSSOProviderReference({
2012
+ ...provider,
2013
+ organizationId: provider.organizationId ?? void 0,
2014
+ oidcConfig,
2015
+ samlConfig
2016
+ });
2017
+ try {
2018
+ await options.guardProviderMutation({
2019
+ ...mutation,
2020
+ provider: {
2021
+ id: provider.id,
2022
+ providerId: provider.providerId,
2023
+ organizationId: provider.organizationId ?? null
2024
+ },
2025
+ providerReference
2026
+ }, { database });
2027
+ } catch {
2028
+ try {
2029
+ logger.error("SSO provider mutation guard rejected the mutation");
2030
+ } catch {}
2031
+ throw new APIError("CONFLICT", {
2032
+ code: "SSO_PROVIDER_MUTATION_REJECTED",
2033
+ message: "SSO provider mutation is not allowed"
2034
+ });
2035
+ }
2036
+ }
2037
+ async function assertProviderMutationGuardCapabilities(options, adapter) {
2038
+ if (!options?.guardProviderMutation) return;
2039
+ assertSSONativeTransactionSupport(adapter, {
2040
+ code: "SSO_PROVIDER_MUTATION_GUARD_REQUIRES_NATIVE_TRANSACTIONS",
2041
+ message: "SSO provider mutation guards require a database adapter with native transaction support"
2042
+ });
2043
+ await assertSSOAsyncContextSupport({
2044
+ code: "SSO_PROVIDER_MUTATION_GUARD_REQUIRES_ASYNC_CONTEXT",
2045
+ message: "SSO provider mutation guards require database transaction async context support"
1643
2046
  });
1644
2047
  }
1645
2048
  async function lockSSOProviderForAccountLink(ctx, provider) {
1646
2049
  if (typeof provider.id !== "string") return;
1647
- const lockedProvider = await lockSSOProviderRow(ctx.context.adapter, provider.providerId);
2050
+ const lockedProvider = await lockSSOProviderRow(ctx.context, {
2051
+ id: provider.id,
2052
+ providerId: provider.providerId
2053
+ });
1648
2054
  if (!lockedProvider) throw new APIError("CONFLICT", {
1649
2055
  code: "SSO_PROVIDER_CHANGED",
1650
2056
  message: "SSO provider changed while account linking was in progress"
@@ -1913,10 +2319,14 @@ const updateSSOProvider = (options) => {
1913
2319
  const { issuer, domain, samlConfig, oidcConfig } = body;
1914
2320
  const additionalFields = parseSSOProviderAdditionalFields(options, body, "update");
1915
2321
  if (!issuer && !domain && !samlConfig && !oidcConfig && Object.keys(additionalFields).length === 0) throw new APIError("BAD_REQUEST", { message: "No fields provided for update" });
1916
- await checkProviderAccess(ctx, providerId);
2322
+ const authorizedProvider = await checkProviderAccess(ctx, providerId);
2323
+ await assertProviderMutationGuardCapabilities(options, ctx.context.adapter);
1917
2324
  const fullProvider = await runWithTransaction(ctx.context.adapter, async () => {
1918
2325
  const trx = await getCurrentAdapter(ctx.context.adapter);
1919
- const existingProvider = await lockSSOProviderRow(ctx.context.adapter, providerId);
2326
+ const existingProvider = await lockSSOProviderRow(ctx.context, {
2327
+ id: authorizedProvider.id,
2328
+ providerId
2329
+ });
1920
2330
  if (!existingProvider) throw new APIError("NOT_FOUND", { message: "Provider not found" });
1921
2331
  const updateData = { ...additionalFields };
1922
2332
  let providerIdentityBoundaryChanged = body.issuer !== void 0 && body.issuer !== existingProvider.issuer;
@@ -1926,10 +2336,9 @@ const updateSSOProvider = (options) => {
1926
2336
  if (body.domain !== existingProvider.domain) updateData.domainVerified = false;
1927
2337
  }
1928
2338
  if (body.samlConfig) {
1929
- if (body.samlConfig.idpMetadata?.metadata) {
1930
- const maxMetadataSize = options?.saml?.maxMetadataSize ?? 102400;
1931
- if (new TextEncoder().encode(body.samlConfig.idpMetadata.metadata).length > maxMetadataSize) throw new APIError("BAD_REQUEST", { message: `IdP metadata exceeds maximum allowed size (${maxMetadataSize} bytes)` });
1932
- }
2339
+ const maxMetadataSize = options?.saml?.maxMetadataSize ?? 102400;
2340
+ assertSAMLMetadataSize(body.samlConfig.idpMetadata?.metadata, "IdP", maxMetadataSize);
2341
+ assertSAMLMetadataSize(body.samlConfig.spMetadata?.metadata, "SP", maxMetadataSize);
1933
2342
  if (body.samlConfig.signatureAlgorithm !== void 0 || body.samlConfig.digestAlgorithm !== void 0) validateConfigAlgorithms({
1934
2343
  signatureAlgorithm: body.samlConfig.signatureAlgorithm,
1935
2344
  digestAlgorithm: body.samlConfig.digestAlgorithm
@@ -1938,6 +2347,7 @@ const updateSSOProvider = (options) => {
1938
2347
  const updatedSamlConfig = mergeSAMLConfig(currentSamlConfig, body.samlConfig, updateData.issuer || currentSamlConfig.issuer || existingProvider.issuer);
1939
2348
  validateCertSources(updatedSamlConfig);
1940
2349
  assertSAMLIdentityProviderAuthority(updatedSamlConfig);
2350
+ assertSAMLServiceProviderMetadataPolicy(updatedSamlConfig);
1941
2351
  if (samlIdentityBoundaryChanged(currentSamlConfig, updatedSamlConfig)) providerIdentityBoundaryChanged = true;
1942
2352
  updateData.samlConfig = JSON.stringify(updatedSamlConfig);
1943
2353
  }
@@ -1955,6 +2365,10 @@ const updateSSOProvider = (options) => {
1955
2365
  if (oidcIdentityBoundaryChanged(currentOidcConfig, updatedOidcConfig)) providerIdentityBoundaryChanged = true;
1956
2366
  updateData.oidcConfig = JSON.stringify(updatedOidcConfig);
1957
2367
  }
2368
+ await guardSSOProviderMutation(options, {
2369
+ action: "update",
2370
+ isAuthenticationBoundaryChange: providerIdentityBoundaryChanged
2371
+ }, existingProvider, trx, ctx.context.logger);
1958
2372
  if (providerIdentityBoundaryChanged) {
1959
2373
  if (await trx.findOne({
1960
2374
  model: "account",
@@ -1985,7 +2399,7 @@ const updateSSOProvider = (options) => {
1985
2399
  return ctx.json(sanitizeProvider(fullProvider, ctx.context.baseURL, options));
1986
2400
  });
1987
2401
  };
1988
- const deleteSSOProvider = () => {
2402
+ const deleteSSOProvider = (options) => {
1989
2403
  return createAuthEndpoint("/sso/delete-provider", {
1990
2404
  method: "POST",
1991
2405
  use: [sessionMiddleware],
@@ -2002,9 +2416,16 @@ const deleteSSOProvider = () => {
2002
2416
  } }
2003
2417
  }, async (ctx) => {
2004
2418
  const { providerId } = ctx.body;
2005
- await checkProviderAccess(ctx, providerId);
2419
+ const authorizedProvider = await checkProviderAccess(ctx, providerId);
2420
+ await assertProviderMutationGuardCapabilities(options, ctx.context.adapter);
2006
2421
  await runWithTransaction(ctx.context.adapter, async () => {
2007
2422
  const trx = await getCurrentAdapter(ctx.context.adapter);
2423
+ const existingProvider = await lockSSOProviderRow(ctx.context, {
2424
+ id: authorizedProvider.id,
2425
+ providerId
2426
+ });
2427
+ if (!existingProvider) throw new APIError("NOT_FOUND", { message: "Provider not found" });
2428
+ await guardSSOProviderMutation(options, { action: "delete" }, existingProvider, trx, ctx.context.logger);
2008
2429
  await trx.deleteMany({
2009
2430
  model: "account",
2010
2431
  where: [{
@@ -2015,6 +2436,9 @@ const deleteSSOProvider = () => {
2015
2436
  await trx.delete({
2016
2437
  model: "ssoProvider",
2017
2438
  where: [{
2439
+ field: "id",
2440
+ value: existingProvider.id
2441
+ }, {
2018
2442
  field: "providerId",
2019
2443
  value: providerId
2020
2444
  }]
@@ -2144,82 +2568,8 @@ const verifyDomain = (options) => {
2144
2568
  });
2145
2569
  };
2146
2570
  //#endregion
2147
- //#region src/provider-reference.ts
2148
- const SSO_PROVIDER_STATE_KEY = "ssoProviderReference";
2149
- function serializeCanonical(value) {
2150
- if (value === null) return "null";
2151
- if (Array.isArray(value)) return `[${value.map((entry) => entry === void 0 ? "null" : serializeCanonical(entry)).join(",")}]`;
2152
- switch (typeof value) {
2153
- case "boolean":
2154
- case "number":
2155
- case "string": return JSON.stringify(value);
2156
- case "object": return `{${Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => `${JSON.stringify(key)}:${serializeCanonical(entry)}`).join(",")}}`;
2157
- default: throw new TypeError("SSO provider configuration must be JSON-serializable");
2158
- }
2159
- }
2160
- function withoutOIDCSecret(configuration) {
2161
- if (!configuration) return void 0;
2162
- const result = { ...configuration };
2163
- result.clientSecret = void 0;
2164
- return result;
2165
- }
2166
- function getProviderSource(provider) {
2167
- return typeof provider.id === "string" && provider.id.length > 0 ? {
2168
- type: "persisted",
2169
- recordId: provider.id
2170
- } : { type: "configured" };
2171
- }
2172
- async function computeProviderAuthenticationFingerprint(provider) {
2173
- const digest = await createHash("SHA-256").digest(serializeCanonical({
2174
- domain: provider.domain,
2175
- domainVerified: "domainVerified" in provider ? provider.domainVerified : void 0,
2176
- issuer: provider.issuer,
2177
- organizationId: provider.organizationId,
2178
- oidcConfig: withoutOIDCSecret(provider.oidcConfig)
2179
- }));
2180
- return base64Url.encode(new Uint8Array(digest), { padding: false });
2181
- }
2182
- async function computeSSOProviderReference(provider) {
2183
- return {
2184
- providerId: provider.providerId,
2185
- source: getProviderSource(provider),
2186
- authenticationConfigurationFingerprint: await computeProviderAuthenticationFingerprint(provider)
2187
- };
2188
- }
2189
- function isRecord$1(value) {
2190
- return typeof value === "object" && value !== null && !Array.isArray(value);
2191
- }
2192
- function parseSSOProviderReference(value) {
2193
- if (!isRecord$1(value) || !isRecord$1(value.source)) return null;
2194
- const providerId = value.providerId;
2195
- const authenticationConfigurationFingerprint = value.authenticationConfigurationFingerprint;
2196
- const source = value.source;
2197
- if (typeof providerId !== "string" || providerId.length === 0 || typeof authenticationConfigurationFingerprint !== "string" || authenticationConfigurationFingerprint.length === 0) return null;
2198
- if (source.type === "configured") return {
2199
- providerId,
2200
- source: { type: "configured" },
2201
- authenticationConfigurationFingerprint
2202
- };
2203
- if (source.type === "persisted" && typeof source.recordId === "string" && source.recordId.length > 0) return {
2204
- providerId,
2205
- source: {
2206
- type: "persisted",
2207
- recordId: source.recordId
2208
- },
2209
- authenticationConfigurationFingerprint
2210
- };
2211
- return null;
2212
- }
2213
- async function isCurrentSSOProviderReference(provider, reference) {
2214
- if (!reference || reference.providerId !== provider.providerId) return false;
2215
- const source = getProviderSource(provider);
2216
- if (source.type !== reference.source.type) return false;
2217
- if (source.type === "persisted" && (reference.source.type !== "persisted" || source.recordId !== reference.source.recordId)) return false;
2218
- return reference.authenticationConfigurationFingerprint === await computeProviderAuthenticationFingerprint(provider);
2219
- }
2220
- //#endregion
2221
2571
  //#region src/saml-state.ts
2222
- async function generateRelayState(c, link) {
2572
+ async function generateRelayState(c, link, providerReference) {
2223
2573
  const callbackURL = c.body.callbackURL;
2224
2574
  if (!callbackURL) throw new APIError("BAD_REQUEST", { message: "callbackURL is required" });
2225
2575
  const stateData = {
@@ -2232,7 +2582,8 @@ async function generateRelayState(c, link) {
2232
2582
  * This is the actual expiry time of the state
2233
2583
  */
2234
2584
  expiresAt: Date.now() + 600 * 1e3,
2235
- requestSignUp: c.body.requestSignUp
2585
+ requestSignUp: c.body.requestSignUp,
2586
+ serverContext: providerReference ? { [SSO_PROVIDER_STATE_KEY]: providerReference } : void 0
2236
2587
  };
2237
2588
  try {
2238
2589
  return generateGenericState(c, stateData, { cookieName: "relay_state" });
@@ -2268,81 +2619,6 @@ async function parseRelayState(c) {
2268
2619
  return parsedData;
2269
2620
  }
2270
2621
  //#endregion
2271
- //#region src/user-resolution.ts
2272
- const SSO_AUTHENTICATION_FAILURE = Symbol("SSO_AUTHENTICATION_FAILURE");
2273
- function isRecord(value) {
2274
- return typeof value === "object" && value !== null && !Array.isArray(value);
2275
- }
2276
- function isNonEmptyString(value) {
2277
- return typeof value === "string" && value.trim().length > 0;
2278
- }
2279
- function isSSOUserResolution(value) {
2280
- if (!isRecord(value)) return false;
2281
- if (value.action === "continue") return true;
2282
- if (value.action === "link") return isNonEmptyString(value.userId) && (value.profile === "preserve" || value.profile === "update");
2283
- return value.action === "reject" && isNonEmptyString(value.code) && (value.message === void 0 || typeof value.message === "string");
2284
- }
2285
- function logFailure(logger, message, error) {
2286
- try {
2287
- logger.error(message, ...error === void 0 ? [] : [error]);
2288
- } catch {}
2289
- }
2290
- function resolutionFailure() {
2291
- return new APIError("INTERNAL_SERVER_ERROR", {
2292
- code: "SSO_USER_RESOLUTION_FAILED",
2293
- message: "Unable to resolve the SSO user"
2294
- });
2295
- }
2296
- async function resolveSSOUser(resolveUser, input, database, logger) {
2297
- let resolution;
2298
- try {
2299
- resolution = await resolveUser(input, { database });
2300
- } catch (error) {
2301
- logFailure(logger, "SSO user resolution failed", error);
2302
- throw resolutionFailure();
2303
- }
2304
- if (!isSSOUserResolution(resolution)) {
2305
- logFailure(logger, "SSO user resolver returned an invalid decision");
2306
- throw resolutionFailure();
2307
- }
2308
- return resolution;
2309
- }
2310
- function assertSSOUserResolutionNativeTransactionSupport(adapter) {
2311
- if (typeof adapter.options?.adapterConfig.transaction === "function") return;
2312
- throw new APIError("NOT_IMPLEMENTED", {
2313
- code: "SSO_USER_RESOLUTION_REQUIRES_NATIVE_TRANSACTIONS",
2314
- message: "SSO user resolution requires a database adapter with native transaction support"
2315
- });
2316
- }
2317
- async function assertSSOUserResolutionAsyncContextSupport(getStorage = getCurrentDBAdapterAsyncLocalStorage) {
2318
- try {
2319
- await getStorage();
2320
- } catch {
2321
- throw new APIError("NOT_IMPLEMENTED", {
2322
- code: "SSO_USER_RESOLUTION_REQUIRES_ASYNC_CONTEXT",
2323
- message: "SSO user resolution requires database transaction async context support"
2324
- });
2325
- }
2326
- }
2327
- function assertSSOUserResolutionSessionStorage(options) {
2328
- if (!options.secondaryStorage || options.session?.storeSessionInDatabase === true && options.session.preserveSessionInDatabase !== true) return;
2329
- throw new APIError("NOT_IMPLEMENTED", {
2330
- code: "SSO_USER_RESOLUTION_REQUIRES_DATABASE_SESSIONS",
2331
- message: "SSO user resolution requires database-backed sessions with database fallback"
2332
- });
2333
- }
2334
- function requireSuccessfulSSOAuthentication(result) {
2335
- if (!result.error) return result;
2336
- throw Object.assign(/* @__PURE__ */ new Error("SSO authentication failed"), {
2337
- [SSO_AUTHENTICATION_FAILURE]: true,
2338
- result
2339
- });
2340
- }
2341
- function getFailedSSOAuthenticationResult(error) {
2342
- if (!(error instanceof Error) || !(SSO_AUTHENTICATION_FAILURE in error) || error[SSO_AUTHENTICATION_FAILURE] !== true || !("result" in error)) return;
2343
- return error.result;
2344
- }
2345
- //#endregion
2346
2622
  //#region src/saml/timestamp.ts
2347
2623
  /**
2348
2624
  * Validates SAML assertion timestamp conditions (NotBefore/NotOnOrAfter).
@@ -2385,6 +2661,10 @@ function validateSAMLTimestamp(conditions, options = {}) {
2385
2661
  }
2386
2662
  //#endregion
2387
2663
  //#region src/routes/saml-pipeline.ts
2664
+ function isSameSSOProviderReference(left, right) {
2665
+ if (left.providerId !== right.providerId || left.authenticationConfigurationFingerprint !== right.authenticationConfigurationFingerprint || left.source.type !== right.source.type) return false;
2666
+ return left.source.type === "configured" ? true : right.source.type === "persisted" && left.source.recordId === right.source.recordId;
2667
+ }
2388
2668
  function getSafeRedirectCandidate(url, callbackPathname, appOrigin, isTrustedOrigin) {
2389
2669
  if (!url) return;
2390
2670
  if (url.startsWith("/")) {
@@ -2499,10 +2779,11 @@ async function processSAMLResponse(ctx, params, options) {
2499
2779
  if (new TextEncoder().encode(params.SAMLResponse).length > maxResponseSize) throw new APIError("BAD_REQUEST", { message: `SAML response exceeds maximum allowed size (${maxResponseSize} bytes)` });
2500
2780
  const SAMLResponse = params.SAMLResponse.replace(/\s+/g, "");
2501
2781
  let relayState = null;
2782
+ let relayStateValidationFailed = false;
2502
2783
  if (params.RelayState) try {
2503
2784
  relayState = await parseRelayState(ctx);
2504
2785
  } catch {
2505
- relayState = null;
2786
+ relayStateValidationFailed = true;
2506
2787
  }
2507
2788
  const provider = await findSAMLProvider(providerId, options, ctx.context.adapter);
2508
2789
  if (!provider?.samlConfig) throw new APIError("NOT_FOUND", { message: "No SAML provider found" });
@@ -2515,6 +2796,7 @@ async function processSAMLResponse(ctx, params, options) {
2515
2796
  const samlRedirectUrl = getSafeRedirectUrl(redirectCandidates, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
2516
2797
  const samlErrorRedirectUrl = getSafeRedirectUrl([relayState?.errorURL, samlRedirectUrl], currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
2517
2798
  params.onErrorRedirectResolved?.(samlErrorRedirectUrl);
2799
+ const stateProviderReference = parseSSOProviderReference(relayState?.serverContext?.[SSO_PROVIDER_STATE_KEY]);
2518
2800
  validateSingleAssertion(SAMLResponse);
2519
2801
  let parsedResponse;
2520
2802
  try {
@@ -2523,15 +2805,9 @@ async function processSAMLResponse(ctx, params, options) {
2523
2805
  RelayState: params.RelayState || void 0
2524
2806
  } });
2525
2807
  if (!parsedResponse?.extract) throw new Error("Invalid SAML response structure");
2526
- } catch (error) {
2527
- ctx.context.logger.error("SAML response validation failed", {
2528
- error,
2529
- samlResponsePreview: SAMLResponse.slice(0, 200)
2530
- });
2531
- throw new APIError("BAD_REQUEST", {
2532
- message: "Invalid SAML response",
2533
- details: error instanceof Error ? error.message : String(error)
2534
- });
2808
+ } catch {
2809
+ ctx.context.logger.error("SAML response validation failed");
2810
+ throw new APIError("BAD_REQUEST", { message: "Invalid SAML response" });
2535
2811
  }
2536
2812
  const { extract } = parsedResponse;
2537
2813
  const samlContent = parsedResponse.samlContent;
@@ -2544,9 +2820,14 @@ async function processSAMLResponse(ctx, params, options) {
2544
2820
  const expectedAudiences = [sp.entityMeta.getEntityID(), parsedSamlConfig.audience];
2545
2821
  const assertionConsumerServiceUrl = sp.entityMeta.getAssertionConsumerService(SAML_HTTP_POST_BINDING);
2546
2822
  const expectedRecipients = getExpectedSAMLRecipients(parsedSamlConfig, ctx.context.baseURL, providerId, currentCallbackPath, assertionConsumerServiceUrl);
2823
+ const serviceProviderPolicy = deriveSAMLServiceProviderPolicy(parsedSamlConfig);
2547
2824
  let samlBindingContent;
2548
2825
  try {
2549
2826
  samlBindingContent = await getSAMLResponseBindingContent(sp, samlContent);
2827
+ if (serviceProviderPolicy.wantAssertionsSigned) verifySAMLAssertionSignature(samlBindingContent, {
2828
+ metadata: idp.entityMeta,
2829
+ signatureAlgorithm: idp.entitySetting.requestSignatureAlgorithm
2830
+ });
2550
2831
  validateSAMLResponseBinding(samlBindingContent, {
2551
2832
  expectedAudiences,
2552
2833
  expectedRecipients
@@ -2555,27 +2836,20 @@ async function processSAMLResponse(ctx, params, options) {
2555
2836
  if (isAPIError(error)) {
2556
2837
  ctx.context.logger.error("SAML response binding validation failed", {
2557
2838
  providerId,
2558
- code: error.body?.code,
2559
- expectedAudiences: expectedAudiences.filter(Boolean),
2560
- expectedRecipients: expectedRecipients.filter(Boolean)
2839
+ code: error.body?.code
2561
2840
  });
2562
2841
  throw ctx.redirect(buildSAMLRedirectUrl(samlErrorRedirectUrl, {
2563
2842
  error: "invalid_saml_response",
2564
2843
  error_description: error.body?.message || error.message || "Invalid SAML response"
2565
2844
  }));
2566
2845
  }
2567
- ctx.context.logger.error("SAML response binding validation failed", {
2568
- providerId,
2569
- error,
2570
- expectedAudiences: expectedAudiences.filter(Boolean),
2571
- expectedRecipients: expectedRecipients.filter(Boolean)
2572
- });
2846
+ ctx.context.logger.error("SAML response binding validation failed", { providerId });
2573
2847
  throw ctx.redirect(buildSAMLRedirectUrl(samlErrorRedirectUrl, {
2574
2848
  error: "invalid_saml_response",
2575
2849
  error_description: "SAML response binding could not be validated"
2576
2850
  }));
2577
2851
  }
2578
- await validateInResponseTo(ctx, {
2852
+ const requestProviderReference = (await validateInResponseTo(ctx, {
2579
2853
  extract,
2580
2854
  providerId,
2581
2855
  options: {
@@ -2583,7 +2857,24 @@ async function processSAMLResponse(ctx, params, options) {
2583
2857
  allowIdpInitiated: options?.saml?.allowIdpInitiated
2584
2858
  },
2585
2859
  redirectUrl: samlErrorRedirectUrl
2586
- });
2860
+ }))?.providerReference;
2861
+ if (relayStateValidationFailed) throw ctx.redirect(buildSAMLRedirectUrl(samlErrorRedirectUrl, {
2862
+ error: "invalid_state",
2863
+ error_description: "invalid_or_expired_relay_state"
2864
+ }));
2865
+ if (relayState && !stateProviderReference) throw ctx.redirect(buildSAMLRedirectUrl(samlErrorRedirectUrl, {
2866
+ error: "invalid_state",
2867
+ error_description: "sso_provider_reference_missing_or_invalid"
2868
+ }));
2869
+ if (stateProviderReference && requestProviderReference && !isSameSSOProviderReference(stateProviderReference, requestProviderReference)) throw ctx.redirect(buildSAMLRedirectUrl(samlErrorRedirectUrl, {
2870
+ error: "invalid_state",
2871
+ error_description: "sso_provider_reference_mismatch"
2872
+ }));
2873
+ const providerReference = stateProviderReference ?? requestProviderReference ?? await computeSSOProviderReference(provider);
2874
+ if (!await isCurrentSSOProviderReference(provider, providerReference)) throw ctx.redirect(buildSAMLRedirectUrl(samlErrorRedirectUrl, {
2875
+ error: "invalid_state",
2876
+ error_description: "sso_provider_changed_during_authentication"
2877
+ }));
2587
2878
  validateAudience(ctx, {
2588
2879
  extract,
2589
2880
  expectedAudience: parsedSamlConfig.audience || sp.entityMeta.getEntityID(),
@@ -2619,6 +2910,9 @@ async function processSAMLResponse(ctx, params, options) {
2619
2910
  }
2620
2911
  } else ctx.context.logger.warn("Could not extract assertion ID for replay protection", { providerId });
2621
2912
  const attributes = extract.attributes || {};
2913
+ const providerAttributes = {};
2914
+ for (const [name, value] of Object.entries(attributes)) if (typeof value === "string") providerAttributes[name] = value;
2915
+ else if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) providerAttributes[name] = value;
2622
2916
  const mapping = parsedSamlConfig.mapping ?? {};
2623
2917
  const attr = (key) => {
2624
2918
  const value = attributes[key];
@@ -2633,31 +2927,63 @@ async function processSAMLResponse(ctx, params, options) {
2633
2927
  };
2634
2928
  if (!userInfo.id || !userInfo.email) {
2635
2929
  ctx.context.logger.error("Missing essential user info from SAML response", {
2636
- attributes: Object.keys(attributes),
2637
- mapping,
2638
- extractedId: userInfo.id,
2639
- extractedEmail: userInfo.email
2930
+ providerId,
2931
+ attributeNames: Object.keys(attributes),
2932
+ hasNameId: Boolean(userInfo.id),
2933
+ hasEmail: Boolean(userInfo.email)
2640
2934
  });
2641
2935
  throw new APIError("BAD_REQUEST", { message: "Unable to extract user ID or email from SAML response" });
2642
2936
  }
2937
+ const providerUserAttributes = Object.fromEntries(Object.entries(userInfo).filter(([key]) => key !== "id"));
2938
+ const providerUser = {
2939
+ ...providerUserAttributes,
2940
+ email: userInfo.email,
2941
+ name: userInfo.name || userInfo.email,
2942
+ image: typeof providerUserAttributes.image === "string" ? providerUserAttributes.image : void 0,
2943
+ emailVerified: userInfo.emailVerified
2944
+ };
2945
+ const accountKey = {
2946
+ issuer,
2947
+ providerAccountId: userInfo.id
2948
+ };
2643
2949
  const isTrustedProvider = "domainVerified" in provider && !!provider.domainVerified && validateEmailDomain(userInfo.email, provider.domain);
2644
2950
  const callbackUrl = redirectCandidates.some(Boolean) ? samlRedirectUrl : ctx.context.baseURL;
2645
2951
  const errorUrl = samlErrorRedirectUrl;
2646
2952
  let result;
2647
2953
  try {
2954
+ if (options?.resolveUser) {
2955
+ assertSSOUserResolutionNativeTransactionSupport(ctx.context.adapter);
2956
+ assertSSOUserResolutionSessionStorage(ctx.context.options);
2957
+ await assertSSOUserResolutionAsyncContextSupport();
2958
+ }
2648
2959
  result = await runWithTransaction(ctx.context.adapter, async () => {
2649
2960
  await lockSSOProviderForAccountLink(ctx, provider);
2650
- return handleOAuthUserInfo(ctx, {
2961
+ const currentProvider = await findSAMLProvider(providerId, options, await getCurrentAdapter(ctx.context.adapter));
2962
+ if (!currentProvider || !await isCurrentSSOProviderReference(currentProvider, providerReference)) throw new APIError("CONFLICT", {
2963
+ code: "SSO_PROVIDER_CHANGED",
2964
+ message: "SSO provider changed while account linking was in progress"
2965
+ });
2966
+ const resolution = options?.resolveUser ? await resolveSSOUser(options.resolveUser, {
2967
+ protocol: "saml",
2968
+ providerId: provider.providerId,
2969
+ accountKey,
2970
+ providerUser,
2971
+ providerAttributes,
2972
+ providerReference
2973
+ }, await getCurrentAdapter(ctx.context.adapter), ctx.context.logger) : void 0;
2974
+ if (resolution?.action === "reject") throw new APIError("FORBIDDEN", {
2975
+ code: resolution.code,
2976
+ ...resolution.message === void 0 ? {} : { message: resolution.message }
2977
+ });
2978
+ const authentication = await handleOAuthUserInfo(ctx, {
2651
2979
  userInfo: {
2652
- email: userInfo.email,
2653
- name: userInfo.name || userInfo.email,
2654
- id: userInfo.id,
2655
- emailVerified: userInfo.emailVerified
2980
+ ...providerUser,
2981
+ id: userInfo.id
2656
2982
  },
2657
2983
  account: {
2658
2984
  providerId,
2659
- issuer,
2660
- providerAccountId: userInfo.id,
2985
+ issuer: accountKey.issuer,
2986
+ providerAccountId: accountKey.providerAccountId,
2661
2987
  accessToken: "",
2662
2988
  refreshToken: ""
2663
2989
  },
@@ -2671,15 +2997,26 @@ async function processSAMLResponse(ctx, params, options) {
2671
2997
  }
2672
2998
  },
2673
2999
  isTrustedProvider,
2674
- trustProviderByName: false
3000
+ trustProviderByName: false,
3001
+ selectedUser: resolution?.action === "link" ? {
3002
+ userId: resolution.userId,
3003
+ profile: resolution.profile
3004
+ } : void 0,
3005
+ deferNonDatabaseWrites: !!options?.resolveUser,
3006
+ requireExactAccountBinding: !!options?.resolveUser
2675
3007
  });
2676
- });
3008
+ return options?.resolveUser ? requireSuccessfulSSOAuthentication(authentication) : authentication;
3009
+ }, { onAfterCommitHookError() {
3010
+ ctx.context.logger.error("Committed SSO authentication after-hook failed");
3011
+ } });
2677
3012
  } catch (e) {
2678
- if (isAPIError(e) && e.body?.code) throw ctx.redirect(buildSAMLRedirectUrl(errorUrl, {
3013
+ const failedAuthentication = getFailedSSOAuthenticationResult(e);
3014
+ if (failedAuthentication) result = failedAuthentication;
3015
+ else if (isAPIError(e) && e.body?.code) throw ctx.redirect(buildSAMLRedirectUrl(errorUrl, {
2679
3016
  error: e.body.code,
2680
3017
  ...e.body.message ? { error_description: e.body.message } : {}
2681
3018
  }));
2682
- throw e;
3019
+ else throw e;
2683
3020
  }
2684
3021
  if (result.error) throw ctx.redirect(buildSAMLRedirectUrl(callbackUrl, { error: result.error.split(" ").join("_") }));
2685
3022
  const { session, user } = result.data;
@@ -2701,6 +3038,7 @@ async function processSAMLResponse(ctx, params, options) {
2701
3038
  provider,
2702
3039
  provisioningOptions: options?.organizationProvisioning
2703
3040
  });
3041
+ if ("accountCookie" in result && result.accountCookie) await setAccountCookie(ctx, result.accountCookie);
2704
3042
  await setSessionCookie(ctx, {
2705
3043
  session,
2706
3044
  user
@@ -2953,9 +3291,10 @@ const registerSSOProvider = (options) => {
2953
3291
  })).length >= limit) throw new APIError("FORBIDDEN", { message: "You have reached the maximum number of SSO providers" });
2954
3292
  const body = ctx.body;
2955
3293
  const additionalFields = parseSSOProviderAdditionalFields(options, body, "create");
2956
- if (body.samlConfig?.idpMetadata?.metadata) {
3294
+ if (body.samlConfig) {
2957
3295
  const maxMetadataSize = options?.saml?.maxMetadataSize ?? 102400;
2958
- if (new TextEncoder().encode(body.samlConfig.idpMetadata.metadata).length > maxMetadataSize) throw new APIError("BAD_REQUEST", { message: `IdP metadata exceeds maximum allowed size (${maxMetadataSize} bytes)` });
3296
+ assertSAMLMetadataSize(body.samlConfig.idpMetadata?.metadata, "IdP", maxMetadataSize);
3297
+ assertSAMLMetadataSize(body.samlConfig.spMetadata?.metadata, "SP", maxMetadataSize);
2959
3298
  }
2960
3299
  if (ctx.body.organizationId) {
2961
3300
  const member = await ctx.context.adapter.findOne({
@@ -2981,18 +3320,6 @@ const registerSSOProvider = (options) => {
2981
3320
  ctx.context.logger.warn(`SSO provider registration rejected for reserved providerId: ${body.providerId}`);
2982
3321
  throw new APIError("UNPROCESSABLE_ENTITY", { message: "This providerId is reserved and cannot be used for an SSO provider" });
2983
3322
  }
2984
- if (ctx.context.hasPlugin("scim")) {
2985
- if (await ctx.context.adapter.findOne({
2986
- model: "scimProvider",
2987
- where: [{
2988
- field: "providerId",
2989
- value: body.providerId
2990
- }]
2991
- })) {
2992
- ctx.context.logger.warn(`SSO provider registration rejected for SCIM providerId: ${body.providerId}`);
2993
- throw new APIError("UNPROCESSABLE_ENTITY", { message: "This providerId is already used by a SCIM provider and cannot be used for an SSO provider" });
2994
- }
2995
- }
2996
3323
  if (await ctx.context.adapter.findOne({
2997
3324
  model: "ssoProvider",
2998
3325
  where: [{
@@ -3072,6 +3399,10 @@ const registerSSOProvider = (options) => {
3072
3399
  }, options?.saml?.algorithms);
3073
3400
  validateCertSources(body.samlConfig);
3074
3401
  assertSAMLIdentityProviderAuthority(body.samlConfig);
3402
+ assertSAMLServiceProviderMetadataPolicy({
3403
+ ...body.samlConfig,
3404
+ issuer: body.issuer
3405
+ });
3075
3406
  const hasIdpMetadata = body.samlConfig.idpMetadata?.metadata;
3076
3407
  let hasEntryPoint = false;
3077
3408
  if (body.samlConfig.entryPoint) try {
@@ -3344,7 +3675,8 @@ const signInSSO = (options) => {
3344
3675
  const parsedSamlConfig = typeof provider.samlConfig === "object" ? provider.samlConfig : safeJsonParse(provider.samlConfig);
3345
3676
  if (!parsedSamlConfig) throw new APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
3346
3677
  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" });
3347
- const { state: relayState } = await generateRelayState(ctx, void 0);
3678
+ const providerReference = await computeSSOProviderReference(provider);
3679
+ const { state: relayState } = await generateRelayState(ctx, void 0, providerReference);
3348
3680
  const sp = createSP(parsedSamlConfig, ctx.context.baseURL, provider.providerId, { relayState });
3349
3681
  const idp = createIdP(parsedSamlConfig);
3350
3682
  const loginRequest = sp.createLoginRequest(idp, "redirect");
@@ -3354,6 +3686,7 @@ const signInSSO = (options) => {
3354
3686
  const record = {
3355
3687
  id: loginRequest.id,
3356
3688
  providerId: provider.providerId,
3689
+ providerReference,
3357
3690
  createdAt: Date.now(),
3358
3691
  expiresAt: Date.now() + ttl
3359
3692
  };
@@ -3422,7 +3755,8 @@ async function handleOIDCCallback(ctx, options, providerId, stateData, parsedPro
3422
3755
  if (!code || error) redirectOIDCError(error || "invalid_request", error_description || (error ? error : "authorization_code_not_found"));
3423
3756
  const provider = await resolveOIDCProvider(ctx, options, providerId);
3424
3757
  if (!provider) throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=provider not found`);
3425
- if (!await isCurrentSSOProviderReference(provider, providerReference)) redirectOIDCError("invalid_state", "sso_provider_changed_during_authentication");
3758
+ const acceptedProviderReference = providerReference ?? redirectOIDCError("invalid_state", "missing_sso_provider_reference");
3759
+ if (!await isCurrentSSOProviderReference(provider, acceptedProviderReference)) redirectOIDCError("invalid_state", "sso_provider_changed_during_authentication");
3426
3760
  if (options?.domainVerification?.enabled && !("domainVerified" in provider && provider.domainVerified)) throw new APIError("UNAUTHORIZED", { message: "Provider domain has not been verified" });
3427
3761
  let config = provider.oidcConfig;
3428
3762
  if (!config) throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=provider not found`);
@@ -3570,7 +3904,7 @@ async function handleOIDCCallback(ctx, options, providerId, stateData, parsedPro
3570
3904
  linked = await runWithTransaction(ctx.context.adapter, async () => {
3571
3905
  await lockSSOProviderForAccountLink(ctx, provider);
3572
3906
  const currentProvider = await resolveOIDCProvider(ctx, options, providerId, await getCurrentAdapter(ctx.context.adapter));
3573
- if (!currentProvider || !await isCurrentSSOProviderReference(currentProvider, providerReference)) throw new APIError("CONFLICT", {
3907
+ if (!currentProvider || !await isCurrentSSOProviderReference(currentProvider, acceptedProviderReference)) throw new APIError("CONFLICT", {
3574
3908
  code: "SSO_PROVIDER_CHANGED",
3575
3909
  message: "SSO provider changed while account linking was in progress"
3576
3910
  });
@@ -3579,7 +3913,9 @@ async function handleOIDCCallback(ctx, options, providerId, stateData, parsedPro
3579
3913
  providerId: provider.providerId,
3580
3914
  accountKey,
3581
3915
  providerUser,
3582
- providerClaims: rawProfile ?? {}
3916
+ providerClaims: rawProfile ?? {},
3917
+ verifiedIdTokenClaims: verifiedIdToken?.payload ?? {},
3918
+ providerReference: acceptedProviderReference
3583
3919
  }, await getCurrentAdapter(ctx.context.adapter), ctx.context.logger) : void 0;
3584
3920
  if (resolution?.action === "reject") throw new APIError("FORBIDDEN", {
3585
3921
  code: resolution.code,
@@ -4097,7 +4433,7 @@ function sso(options) {
4097
4433
  listSSOProviders: listSSOProviders(optionsWithStore),
4098
4434
  getSSOProvider: getSSOProvider(optionsWithStore),
4099
4435
  updateSSOProvider: updateSSOProvider(optionsWithStore),
4100
- deleteSSOProvider: deleteSSOProvider()
4436
+ deleteSSOProvider: deleteSSOProvider(optionsWithStore)
4101
4437
  };
4102
4438
  if (options?.domainVerification?.enabled) {
4103
4439
  const domainVerificationEndpoints = {
@@ -4206,4 +4542,4 @@ function sso(options) {
4206
4542
  };
4207
4543
  }
4208
4544
  //#endregion
4209
- export { DEFAULT_CLOCK_SKEW_MS, DEFAULT_MAX_SAML_METADATA_SIZE, DEFAULT_MAX_SAML_RESPONSE_SIZE, DataEncryptionAlgorithm, DigestAlgorithm, DiscoveryError, KeyEncryptionAlgorithm, REQUIRED_DISCOVERY_FIELDS, SignatureAlgorithm, computeDiscoveryUrl, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
4545
+ export { DEFAULT_CLOCK_SKEW_MS, DEFAULT_MAX_SAML_METADATA_SIZE, DEFAULT_MAX_SAML_RESPONSE_SIZE, DataEncryptionAlgorithm, DigestAlgorithm, DiscoveryError, KeyEncryptionAlgorithm, REQUIRED_DISCOVERY_FIELDS, SignatureAlgorithm, computeDiscoveryUrl, deriveSAMLIdentityProviderEntityID, deriveSAMLServiceProviderPolicy, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };