@better-auth/sso 1.7.0-rc.1 → 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,24 +1,26 @@
1
- import { t as PACKAGE_VERSION } from "./version-lPD-GbqS.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";
5
5
  import { getHostname } from "tldts";
6
6
  import { generateRandomString } from "better-auth/crypto";
7
7
  import * as z from "zod";
8
- import { getCurrentAdapter, runWithTransaction } from "@better-auth/core/context";
8
+ import { getCurrentAdapter, getCurrentDBAdapterAsyncLocalStorage, runWithTransaction } from "@better-auth/core/context";
9
9
  import { filterOutputFields } from "@better-auth/core/utils/db";
10
10
  import { classifyHost, isPublicRoutableHost } from "@better-auth/core/utils/host";
11
11
  import { betterFetch } from "@better-fetch/fetch";
12
- import { createRemoteJWKSet, customFetch, decodeJwt, jwtVerify } from "jose";
13
- import { base64 } from "@better-auth/utils/base64";
12
+ import { createRemoteJWKSet, customFetch, jwtVerify } from "jose";
13
+ import { base64, base64Url } from "@better-auth/utils/base64";
14
+ import { createHash } from "@better-auth/utils/hash";
15
+ import { DOMParser } from "@xmldom/xmldom";
16
+ import * as samlifyNamespace from "samlify";
17
+ import samlifyDefault from "samlify";
14
18
  import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
15
19
  import { parseInputData, toZodSchema } from "better-auth/db";
16
20
  import { isAPIError } from "@better-auth/core/utils/is-api-error";
17
21
  import { HIDE_METADATA, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, authorizationCodeRequest, createAuthorizationURL, createPrivateKeyJwtClientAssertionGetter, generateGenericState, generateState, getOAuth2Tokens, parseGenericState, parseState } from "better-auth";
18
- import { deleteSessionCookie, setSessionCookie } from "better-auth/cookies";
22
+ import { deleteSessionCookie, setAccountCookie, setSessionCookie } from "better-auth/cookies";
19
23
  import { additionalAuthorizationParamsSchema, handleOAuthUserInfo } from "better-auth/oauth2";
20
- import * as samlifyNamespace from "samlify";
21
- import samlifyDefault from "samlify";
22
24
  //#region src/constants.ts
23
25
  /**
24
26
  * SAML Constants
@@ -60,6 +62,8 @@ const DEFAULT_MAX_SAML_RESPONSE_SIZE = 256 * 1024;
60
62
  const DEFAULT_MAX_SAML_METADATA_SIZE = 100 * 1024;
61
63
  //#endregion
62
64
  //#region src/utils.ts
65
+ const unsafeSAMLRedirectPathPrefix = /^\/(?:\/|\\|%2f|%5c)/i;
66
+ const isSafeSAMLRedirectPath = (url) => url.startsWith("/") && !unsafeSAMLRedirectPathPrefix.test(url);
63
67
  /**
64
68
  * Safely parses a value that might be a JSON string or already a parsed object.
65
69
  * This handles cases where ORMs like Drizzle might return already parsed objects
@@ -515,15 +519,21 @@ async function fetchOIDCEndpointResponse(name, endpoint, init, isTrustedOrigin)
515
519
  if (isHttpRedirectStatus(response.status)) throwRedirectError(name, endpoint, response.status, response.headers.get("location"));
516
520
  return response;
517
521
  }
522
+ /** Enforces the OpenID Connect authorized-party rules for ID Tokens. */
523
+ function assertOIDCAuthorizedParty(payload, clientId) {
524
+ if (Array.isArray(payload.aud) && payload.aud.length > 1 && payload.azp === void 0 || payload.azp !== void 0 && payload.azp !== clientId) throw new Error("OIDC ID token authorized party does not match the client");
525
+ }
518
526
  /**
519
527
  * Validate an OIDC ID token using the same endpoint fetch policy as the rest of
520
528
  * the SSO OIDC flow.
521
529
  */
522
530
  async function validateOIDCIdToken(token, jwksEndpoint, options, isTrustedOrigin) {
523
- return jwtVerify(token, createRemoteJWKSet(new URL(jwksEndpoint), { [customFetch]: (url, init) => fetchOIDCEndpointResponse("jwksEndpoint", url, init, isTrustedOrigin) }), {
531
+ const verified = await jwtVerify(token, createRemoteJWKSet(new URL(jwksEndpoint), { [customFetch]: (url, init) => fetchOIDCEndpointResponse("jwksEndpoint", url, init, isTrustedOrigin) }), {
524
532
  audience: options.audience,
525
533
  issuer: options.issuer
526
534
  });
535
+ if (typeof options.audience === "string") assertOIDCAuthorizedParty(verified.payload, options.audience);
536
+ return verified;
527
537
  }
528
538
  /**
529
539
  * Fetch the OIDC discovery document from the IdP.
@@ -808,6 +818,102 @@ function mapDiscoveryErrorToAPIError(error) {
808
818
  }
809
819
  }
810
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
811
917
  //#region src/saml/parser.ts
812
918
  const xmlParser = new XMLParser({
813
919
  ignoreAttributes: false,
@@ -1021,8 +1127,12 @@ function validateConfigAlgorithms(config, options = {}) {
1021
1127
  });
1022
1128
  }
1023
1129
  }
1130
+ const saml = typeof samlifyNamespace.SPMetadata === "function" && typeof samlifyNamespace.setSchemaValidator === "function" ? samlifyNamespace : samlifyDefault ?? samlifyNamespace;
1024
1131
  //#endregion
1025
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#";
1026
1136
  function countAssertions(xml) {
1027
1137
  let parsed;
1028
1138
  try {
@@ -1062,6 +1172,39 @@ function validateSingleAssertion(samlResponse) {
1062
1172
  code: "SAML_MULTIPLE_ASSERTIONS"
1063
1173
  });
1064
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
+ }
1065
1208
  //#endregion
1066
1209
  //#region src/saml/error-codes.ts
1067
1210
  const SAML_ERROR_CODES = defineErrorCodes({
@@ -1107,6 +1250,7 @@ function validateCertSources(config) {
1107
1250
  //#region src/saml/response-binding.ts
1108
1251
  const SAML_HTTP_POST_BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
1109
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";
1110
1254
  function toNode(value) {
1111
1255
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1112
1256
  return value;
@@ -1138,11 +1282,75 @@ function parseSAMLContent(samlContent) {
1138
1282
  code: "SAML_RESPONSE_INVALID_XML"
1139
1283
  });
1140
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
+ }
1141
1350
  function getSAMLPostAssertionConsumerServiceUrls(metadata) {
1142
1351
  if (!metadata) return [];
1143
1352
  try {
1144
- 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));
1145
- return [...new Set(locations)];
1353
+ return parseSAMLServiceProviderMetadata(metadata).postAssertionConsumerServiceUrls;
1146
1354
  } catch {
1147
1355
  return [];
1148
1356
  }
@@ -1242,7 +1450,7 @@ function errorRedirectUrl(base, error, description) {
1242
1450
  * samlify's parsed output (not at the top level).
1243
1451
  */
1244
1452
  async function validateInResponseTo(c, ctx) {
1245
- if (ctx.options.enableInResponseToValidation === false) return;
1453
+ if (ctx.options.enableInResponseToValidation === false) return null;
1246
1454
  const inResponseTo = ctx.extract.response?.inResponseTo;
1247
1455
  const allowIdpInitiated = ctx.options.allowIdpInitiated ?? false;
1248
1456
  if (inResponseTo) {
@@ -1268,10 +1476,23 @@ async function validateInResponseTo(c, ctx) {
1268
1476
  });
1269
1477
  throw c.redirect(errorRedirectUrl(ctx.redirectUrl, "invalid_saml_response", "Provider mismatch"));
1270
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
+ };
1271
1491
  } else if (!allowIdpInitiated) {
1272
1492
  c.context.logger.error("SAML IdP-initiated SSO rejected: InResponseTo missing and allowIdpInitiated is false", { providerId: ctx.providerId });
1273
1493
  throw c.redirect(errorRedirectUrl(ctx.redirectUrl, "unsolicited_response", "IdP-initiated SSO not allowed"));
1274
1494
  }
1495
+ return null;
1275
1496
  }
1276
1497
  /**
1277
1498
  * Validates the AudienceRestriction of a SAML assertion.
@@ -1302,7 +1523,260 @@ function validateAudience(c, ctx) {
1302
1523
  }
1303
1524
  }
1304
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
+ }
1612
+ //#endregion
1613
+ //#region src/routes/helpers.ts
1614
+ /**
1615
+ * Same as `normalizePem`, but applied across the resolved list of IdP signing
1616
+ * certificates so multi-cert rotation configs survive the line-trim step.
1617
+ */
1618
+ function normalizePemList(certs) {
1619
+ if (!certs) return certs;
1620
+ return certs.map((pem) => normalizePem(pem) ?? pem);
1621
+ }
1622
+ async function findSAMLProvider(providerId, options, adapter) {
1623
+ if (options?.defaultSSO?.length) {
1624
+ const match = options.defaultSSO.find((p) => p.providerId === providerId);
1625
+ if (match) return {
1626
+ ...match,
1627
+ userId: "default",
1628
+ issuer: match.samlConfig?.issuer || "",
1629
+ ...options.domainVerification?.enabled ? { domainVerified: true } : {}
1630
+ };
1631
+ }
1632
+ const res = await adapter.findOne({
1633
+ model: "ssoProvider",
1634
+ where: [{
1635
+ field: "providerId",
1636
+ value: providerId
1637
+ }]
1638
+ });
1639
+ if (!res) return null;
1640
+ return {
1641
+ ...res,
1642
+ samlConfig: res.samlConfig ? safeJsonParse(res.samlConfig) || void 0 : void 0
1643
+ };
1644
+ }
1645
+ function createSP(config, baseURL, providerId, opts) {
1646
+ const spData = config.spMetadata;
1647
+ const sloLocation = `${baseURL}/sso/saml2/sp/slo/${providerId}`;
1648
+ const acsUrl = `${baseURL}/sso/saml2/sp/acs/${providerId}`;
1649
+ let metadata = spData?.metadata;
1650
+ assertSAMLServiceProviderMetadataPolicy(config);
1651
+ if (!metadata) metadata = saml.SPMetadata({
1652
+ entityID: spData?.entityID || config.issuer,
1653
+ assertionConsumerService: [{
1654
+ Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
1655
+ Location: acsUrl
1656
+ }],
1657
+ singleLogoutService: opts?.sloOptions ? [{
1658
+ Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
1659
+ Location: sloLocation
1660
+ }, {
1661
+ Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1662
+ Location: sloLocation
1663
+ }] : void 0,
1664
+ wantAssertionsSigned: config.wantAssertionsSigned || false,
1665
+ authnRequestsSigned: config.authnRequestsSigned || false,
1666
+ nameIDFormat: config.identifierFormat ? [config.identifierFormat] : void 0
1667
+ }).getMetadata() || "";
1668
+ return saml.ServiceProvider({
1669
+ metadata,
1670
+ allowCreate: true,
1671
+ wantLogoutRequestSigned: opts?.sloOptions?.wantLogoutRequestSigned ?? false,
1672
+ wantLogoutResponseSigned: opts?.sloOptions?.wantLogoutResponseSigned ?? false,
1673
+ privateKey: normalizePem(spData?.privateKey || config.privateKey),
1674
+ privateKeyPass: spData?.privateKeyPass,
1675
+ isAssertionEncrypted: spData?.isAssertionEncrypted || false,
1676
+ encPrivateKey: normalizePem(spData?.encPrivateKey),
1677
+ encPrivateKeyPass: spData?.encPrivateKeyPass,
1678
+ relayState: opts?.relayState,
1679
+ clockDrifts: opts?.clockSkew && opts?.clockSkew !== 0 ? [-opts.clockSkew, opts.clockSkew] : void 0
1680
+ });
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
+ }
1718
+ function assertSAMLIdentityProviderAuthority(config) {
1719
+ if (config.idpMetadata?.metadata || config.idpMetadata?.entityID) return;
1720
+ throw new APIError("BAD_REQUEST", { message: "SAML manual IdP configuration requires idpMetadata.entityID; issuer identifies the service provider and cannot identify the IdP" });
1721
+ }
1722
+ function createIdP(config) {
1723
+ assertSAMLIdentityProviderAuthority(config);
1724
+ const idpData = config.idpMetadata;
1725
+ if (idpData?.metadata) return saml.IdentityProvider({
1726
+ metadata: idpData.metadata,
1727
+ privateKey: normalizePem(idpData.privateKey),
1728
+ privateKeyPass: idpData.privateKeyPass,
1729
+ isAssertionEncrypted: idpData.isAssertionEncrypted,
1730
+ encPrivateKey: normalizePem(idpData.encPrivateKey),
1731
+ encPrivateKeyPass: idpData.encPrivateKeyPass
1732
+ });
1733
+ return saml.IdentityProvider({
1734
+ entityID: idpData.entityID,
1735
+ singleSignOnService: idpData.singleSignOnService || [{
1736
+ Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
1737
+ Location: config.entryPoint
1738
+ }],
1739
+ singleLogoutService: idpData.singleLogoutService,
1740
+ signingCert: normalizePemList(resolveSigningCerts(config)),
1741
+ wantAuthnRequestsSigned: config.authnRequestsSigned || false,
1742
+ isAssertionEncrypted: idpData.isAssertionEncrypted || false,
1743
+ encPrivateKey: normalizePem(idpData.encPrivateKey),
1744
+ encPrivateKeyPass: idpData.encPrivateKeyPass
1745
+ });
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
+ }
1754
+ function escapeHtml(str) {
1755
+ if (!str) return "";
1756
+ return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1757
+ }
1758
+ function isSAMLPostBindingLocation(value) {
1759
+ let url;
1760
+ try {
1761
+ url = new URL(value);
1762
+ } catch {
1763
+ return false;
1764
+ }
1765
+ return url.protocol === "http:" || url.protocol === "https:";
1766
+ }
1767
+ function createSAMLPostForm(action, samlParam, samlValue, relayState) {
1768
+ if (!isSAMLPostBindingLocation(action)) throw new APIError("BAD_REQUEST", { message: "SAML POST binding location must be an absolute http or https URL" });
1769
+ const safeAction = escapeHtml(action);
1770
+ const safeSamlParam = escapeHtml(samlParam);
1771
+ const safeSamlValue = escapeHtml(samlValue);
1772
+ const safeRelayState = relayState ? escapeHtml(relayState) : void 0;
1773
+ const html = `<!DOCTYPE html><html><body onload="document.forms[0].submit();"><form method="POST" action="${safeAction}"><input type="hidden" name="${safeSamlParam}" value="${safeSamlValue}" />${safeRelayState ? `<input type="hidden" name="RelayState" value="${safeRelayState}" />` : ""}<noscript><input type="submit" value="Continue" /></noscript></form></body></html>`;
1774
+ return new Response(html, { headers: { "Content-Type": "text/html" } });
1775
+ }
1776
+ //#endregion
1305
1777
  //#region src/routes/schemas.ts
1778
+ const absoluteUrlSchema = z.url();
1779
+ const samlRedirectUrlSchema = z.string().refine((url) => isSafeSAMLRedirectPath(url) || absoluteUrlSchema.safeParse(url).success, { message: "Expected an absolute URL or a relative path starting with /" });
1306
1780
  function getSSOProviderAdditionalFields$1(options) {
1307
1781
  return options?.schema?.ssoProvider?.additionalFields ?? {};
1308
1782
  }
@@ -1327,16 +1801,14 @@ function parseSSOProviderAdditionalFields(options, data, action) {
1327
1801
  action
1328
1802
  });
1329
1803
  }
1330
- const oidcMappingSchema = z.object({
1331
- id: z.string().meta({ description: "Field mapping for user ID (defaults to 'sub')" }),
1804
+ const oidcMappingSchema = z.strictObject({
1332
1805
  email: z.string().meta({ description: "Field mapping for email (defaults to 'email')" }),
1333
1806
  emailVerified: z.string().meta({ description: "Field mapping for email verification (defaults to 'email_verified')" }).optional(),
1334
1807
  name: z.string().meta({ description: "Field mapping for name (defaults to 'name')" }),
1335
1808
  image: z.string().meta({ description: "Field mapping for image (defaults to 'picture')" }).optional(),
1336
1809
  extraFields: z.record(z.string(), z.any()).optional()
1337
1810
  }).optional();
1338
- const samlMappingSchema = z.object({
1339
- id: z.string().meta({ description: "Field mapping for user ID (defaults to 'nameID')" }),
1811
+ const samlMappingSchema = z.strictObject({
1340
1812
  email: z.string().meta({ description: "Field mapping for email (defaults to 'email')" }),
1341
1813
  emailVerified: z.string().meta({ description: "Field mapping for email verification" }).optional(),
1342
1814
  name: z.string().meta({ description: "Field mapping for name (defaults to 'displayName')" }),
@@ -1366,29 +1838,40 @@ const oidcConfigSchema = z.object({
1366
1838
  overrideUserInfo: z.boolean().optional(),
1367
1839
  mapping: oidcMappingSchema
1368
1840
  });
1841
+ const samlIdentityProviderConfigSchema = z.object({
1842
+ cert: signingCertSchema.meta({ description: "IdP signing certificate(s). Pass a single PEM string or an array for rolling rotation. Takes precedence over the top-level `cert`." }).optional(),
1843
+ privateKey: z.string().optional(),
1844
+ privateKeyPass: z.string().optional(),
1845
+ isAssertionEncrypted: z.boolean().optional(),
1846
+ encPrivateKey: z.string().optional(),
1847
+ encPrivateKeyPass: z.string().optional(),
1848
+ singleSignOnService: z.array(z.object({
1849
+ Binding: z.string().meta({ description: "The binding type for the SSO service" }),
1850
+ Location: z.string().url().meta({ description: "The URL for the SSO service" })
1851
+ })).meta({ description: "Single Sign-On service configuration" }).optional(),
1852
+ singleLogoutService: z.array(z.object({
1853
+ Binding: z.string(),
1854
+ Location: z.string().url()
1855
+ })).optional()
1856
+ });
1857
+ const samlIdentityProviderMetadataSchema = z.union([samlIdentityProviderConfigSchema.extend({
1858
+ metadata: z.string().min(1),
1859
+ entityID: z.string().optional()
1860
+ }), samlIdentityProviderConfigSchema.extend({
1861
+ metadata: z.undefined().optional(),
1862
+ entityID: z.string().min(1)
1863
+ })], "idpMetadata.entityID is required when IdP metadata XML is not provided");
1864
+ const samlIdentityProviderMetadataUpdateSchema = samlIdentityProviderConfigSchema.extend({
1865
+ metadata: z.string().min(1).optional(),
1866
+ entityID: z.string().min(1).optional()
1867
+ });
1369
1868
  const samlConfigSchema = z.object({
1370
1869
  entryPoint: z.string().url().meta({ description: "The IdP SSO URL (entry point)" }),
1371
1870
  cert: signingCertSchema.meta({ description: "IdP signing certificate(s). Pass a single PEM string or an array for rolling rotation. Omit when `idpMetadata.metadata` XML carries the certs. When both this and `idpMetadata.cert` are set, `idpMetadata.cert` wins." }).optional(),
1372
1871
  audience: z.string().optional(),
1373
1872
  callbackUrl: z.string().refine((url) => !url.includes("#"), { message: "callbackUrl must not contain a fragment" }).optional(),
1374
- idpMetadata: z.object({
1375
- metadata: z.string().optional(),
1376
- entityID: z.string().optional(),
1377
- cert: signingCertSchema.meta({ description: "IdP signing certificate(s). Pass a single PEM string or an array for rolling rotation. Takes precedence over the top-level `cert`." }).optional(),
1378
- privateKey: z.string().optional(),
1379
- privateKeyPass: z.string().optional(),
1380
- isAssertionEncrypted: z.boolean().optional(),
1381
- encPrivateKey: z.string().optional(),
1382
- encPrivateKeyPass: z.string().optional(),
1383
- singleSignOnService: z.array(z.object({
1384
- Binding: z.string().meta({ description: "The binding type for the SSO service" }),
1385
- Location: z.string().url().meta({ description: "The URL for the SSO service" })
1386
- })).meta({ description: "Single Sign-On service configuration" }).optional(),
1387
- singleLogoutService: z.array(z.object({
1388
- Binding: z.string(),
1389
- Location: z.string().url()
1390
- })).optional()
1391
- }).optional(),
1873
+ idpInitiatedCallbackUrl: samlRedirectUrlSchema.optional(),
1874
+ idpMetadata: samlIdentityProviderMetadataSchema,
1392
1875
  spMetadata: z.object({
1393
1876
  metadata: z.string().optional(),
1394
1877
  entityID: z.string().optional(),
@@ -1423,7 +1906,10 @@ const updateSSOProviderBodySchema = z.object({
1423
1906
  issuer: z.string().url().optional(),
1424
1907
  domain: z.string().optional(),
1425
1908
  oidcConfig: oidcConfigSchema.partial().optional(),
1426
- samlConfig: samlConfigSchema.partial().optional()
1909
+ samlConfig: samlConfigSchema.omit({ idpMetadata: true }).partial().extend({
1910
+ idpInitiatedCallbackUrl: samlRedirectUrlSchema.nullable().optional(),
1911
+ idpMetadata: samlIdentityProviderMetadataUpdateSchema.optional()
1912
+ }).optional()
1427
1913
  });
1428
1914
  function getUpdateSSOProviderBodySchema(options) {
1429
1915
  return updateSSOProviderBodySchema.extend({
@@ -1442,18 +1928,7 @@ const OIDC_IDENTITY_BOUNDARY_FIELDS = [
1442
1928
  "tokenEndpoint",
1443
1929
  "userInfoEndpoint"
1444
1930
  ];
1445
- const SAML_IDENTITY_BOUNDARY_FIELDS = [
1446
- "audience",
1447
- "callbackUrl",
1448
- "entryPoint",
1449
- "identifierFormat"
1450
- ];
1451
- const SAML_IDP_BOUNDARY_FIELDS = [
1452
- "metadata",
1453
- "entityID",
1454
- "singleSignOnService"
1455
- ];
1456
- const SAML_SP_BOUNDARY_FIELDS = ["metadata", "entityID"];
1931
+ const SAML_IDENTITY_BOUNDARY_FIELDS = ["audience", "callbackUrl"];
1457
1932
  function isRecord(value) {
1458
1933
  return typeof value === "object" && value !== null && !Array.isArray(value);
1459
1934
  }
@@ -1469,10 +1944,37 @@ function hasChangedField(current, updated, fields) {
1469
1944
  return fields.some((field) => identityValueChanged(current?.[field], updated?.[field]));
1470
1945
  }
1471
1946
  function oidcIdentityBoundaryChanged(current, updated) {
1472
- return hasChangedField(current, updated, OIDC_IDENTITY_BOUNDARY_FIELDS) || identityValueChanged(current.mapping?.id, updated.mapping?.id);
1947
+ return hasChangedField(current, updated, OIDC_IDENTITY_BOUNDARY_FIELDS);
1473
1948
  }
1474
1949
  function samlIdentityBoundaryChanged(current, updated) {
1475
- return hasChangedField(current, updated, SAML_IDENTITY_BOUNDARY_FIELDS) || identityValueChanged(current.mapping?.id, updated.mapping?.id) || 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));
1476
1978
  }
1477
1979
  function parseConfigSnapshot(config, configType) {
1478
1980
  if (!config) return;
@@ -1488,25 +1990,71 @@ function ssoProviderIdentityBoundaryChanged(current, updated) {
1488
1990
  const updatedOidcConfig = parseConfigSnapshot(updated.oidcConfig, "OIDC");
1489
1991
  return Boolean(currentOidcConfig && (!updatedOidcConfig || oidcIdentityBoundaryChanged(currentOidcConfig, updatedOidcConfig)));
1490
1992
  }
1491
- async function lockSSOProviderRow(adapter, providerId) {
1492
- 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({
1493
2002
  model: "ssoProvider",
1494
- where: [{
1495
- field: "providerId",
1496
- value: providerId
1497
- }],
1498
- update: { providerId }
2003
+ where,
2004
+ update: { providerId: provider.providerId }
1499
2005
  });
1500
2006
  }
1501
- async function lockSSOProviderForAccountLink(ctx, provider) {
1502
- const lockedProvider = await lockSSOProviderRow(ctx.context.adapter, provider.providerId);
1503
- if (!lockedProvider) {
1504
- if (provider.userId === "default") return;
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 {}
1505
2031
  throw new APIError("CONFLICT", {
1506
- code: "SSO_PROVIDER_CHANGED",
1507
- message: "SSO provider changed while account linking was in progress"
2032
+ code: "SSO_PROVIDER_MUTATION_REJECTED",
2033
+ message: "SSO provider mutation is not allowed"
1508
2034
  });
1509
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"
2046
+ });
2047
+ }
2048
+ async function lockSSOProviderForAccountLink(ctx, provider) {
2049
+ if (typeof provider.id !== "string") return;
2050
+ const lockedProvider = await lockSSOProviderRow(ctx.context, {
2051
+ id: provider.id,
2052
+ providerId: provider.providerId
2053
+ });
2054
+ if (!lockedProvider) throw new APIError("CONFLICT", {
2055
+ code: "SSO_PROVIDER_CHANGED",
2056
+ message: "SSO provider changed while account linking was in progress"
2057
+ });
1510
2058
  if (ssoProviderIdentityBoundaryChanged(provider, lockedProvider)) throw new APIError("CONFLICT", {
1511
2059
  code: "SSO_PROVIDER_CHANGED",
1512
2060
  message: "SSO provider changed while account linking was in progress"
@@ -1607,6 +2155,8 @@ function sanitizeProvider(provider, baseURL, options) {
1607
2155
  } : void 0,
1608
2156
  samlConfig: samlConfig ? {
1609
2157
  entryPoint: samlConfig.entryPoint,
2158
+ callbackUrl: samlConfig.callbackUrl,
2159
+ idpInitiatedCallbackUrl: samlConfig.idpInitiatedCallbackUrl,
1610
2160
  audience: samlConfig.audience,
1611
2161
  wantAssertionsSigned: samlConfig.wantAssertionsSigned,
1612
2162
  authnRequestsSigned: samlConfig.authnRequestsSigned,
@@ -1696,6 +2246,15 @@ function parseAndValidateConfig(configString, configType) {
1696
2246
  if (!config) throw new APIError("BAD_REQUEST", { message: `Cannot update ${configType} config for a provider that doesn't have ${configType} configured` });
1697
2247
  return config;
1698
2248
  }
2249
+ function mergeSAMLIdentityProviderMetadata(current, updates) {
2250
+ if (!updates) return current;
2251
+ const config = { idpMetadata: {
2252
+ ...current,
2253
+ ...updates
2254
+ } };
2255
+ assertSAMLIdentityProviderAuthority(config);
2256
+ return config.idpMetadata;
2257
+ }
1699
2258
  function mergeSAMLConfig(current, updates, issuer) {
1700
2259
  return {
1701
2260
  ...current,
@@ -1703,11 +2262,15 @@ function mergeSAMLConfig(current, updates, issuer) {
1703
2262
  issuer,
1704
2263
  entryPoint: updates.entryPoint ?? current.entryPoint,
1705
2264
  cert: updates.cert ?? current.cert,
1706
- spMetadata: updates.spMetadata ?? current.spMetadata,
1707
- idpMetadata: updates.idpMetadata ?? current.idpMetadata,
2265
+ spMetadata: updates.spMetadata ? {
2266
+ ...current.spMetadata,
2267
+ ...updates.spMetadata
2268
+ } : current.spMetadata,
2269
+ idpMetadata: mergeSAMLIdentityProviderMetadata(current.idpMetadata, updates.idpMetadata),
1708
2270
  mapping: updates.mapping ?? current.mapping,
1709
2271
  audience: updates.audience ?? current.audience,
1710
2272
  callbackUrl: updates.callbackUrl ?? current.callbackUrl,
2273
+ idpInitiatedCallbackUrl: updates.idpInitiatedCallbackUrl === null ? void 0 : updates.idpInitiatedCallbackUrl ?? current.idpInitiatedCallbackUrl,
1711
2274
  wantAssertionsSigned: updates.wantAssertionsSigned ?? current.wantAssertionsSigned,
1712
2275
  authnRequestsSigned: updates.authnRequestsSigned ?? current.authnRequestsSigned,
1713
2276
  identifierFormat: updates.identifierFormat ?? current.identifierFormat,
@@ -1756,10 +2319,14 @@ const updateSSOProvider = (options) => {
1756
2319
  const { issuer, domain, samlConfig, oidcConfig } = body;
1757
2320
  const additionalFields = parseSSOProviderAdditionalFields(options, body, "update");
1758
2321
  if (!issuer && !domain && !samlConfig && !oidcConfig && Object.keys(additionalFields).length === 0) throw new APIError("BAD_REQUEST", { message: "No fields provided for update" });
1759
- await checkProviderAccess(ctx, providerId);
2322
+ const authorizedProvider = await checkProviderAccess(ctx, providerId);
2323
+ await assertProviderMutationGuardCapabilities(options, ctx.context.adapter);
1760
2324
  const fullProvider = await runWithTransaction(ctx.context.adapter, async () => {
1761
2325
  const trx = await getCurrentAdapter(ctx.context.adapter);
1762
- const existingProvider = await lockSSOProviderRow(ctx.context.adapter, providerId);
2326
+ const existingProvider = await lockSSOProviderRow(ctx.context, {
2327
+ id: authorizedProvider.id,
2328
+ providerId
2329
+ });
1763
2330
  if (!existingProvider) throw new APIError("NOT_FOUND", { message: "Provider not found" });
1764
2331
  const updateData = { ...additionalFields };
1765
2332
  let providerIdentityBoundaryChanged = body.issuer !== void 0 && body.issuer !== existingProvider.issuer;
@@ -1769,10 +2336,9 @@ const updateSSOProvider = (options) => {
1769
2336
  if (body.domain !== existingProvider.domain) updateData.domainVerified = false;
1770
2337
  }
1771
2338
  if (body.samlConfig) {
1772
- if (body.samlConfig.idpMetadata?.metadata) {
1773
- const maxMetadataSize = options?.saml?.maxMetadataSize ?? 102400;
1774
- if (new TextEncoder().encode(body.samlConfig.idpMetadata.metadata).length > maxMetadataSize) throw new APIError("BAD_REQUEST", { message: `IdP metadata exceeds maximum allowed size (${maxMetadataSize} bytes)` });
1775
- }
2339
+ const maxMetadataSize = options?.saml?.maxMetadataSize ?? 102400;
2340
+ assertSAMLMetadataSize(body.samlConfig.idpMetadata?.metadata, "IdP", maxMetadataSize);
2341
+ assertSAMLMetadataSize(body.samlConfig.spMetadata?.metadata, "SP", maxMetadataSize);
1776
2342
  if (body.samlConfig.signatureAlgorithm !== void 0 || body.samlConfig.digestAlgorithm !== void 0) validateConfigAlgorithms({
1777
2343
  signatureAlgorithm: body.samlConfig.signatureAlgorithm,
1778
2344
  digestAlgorithm: body.samlConfig.digestAlgorithm
@@ -1780,6 +2346,8 @@ const updateSSOProvider = (options) => {
1780
2346
  const currentSamlConfig = parseAndValidateConfig(existingProvider.samlConfig, "SAML");
1781
2347
  const updatedSamlConfig = mergeSAMLConfig(currentSamlConfig, body.samlConfig, updateData.issuer || currentSamlConfig.issuer || existingProvider.issuer);
1782
2348
  validateCertSources(updatedSamlConfig);
2349
+ assertSAMLIdentityProviderAuthority(updatedSamlConfig);
2350
+ assertSAMLServiceProviderMetadataPolicy(updatedSamlConfig);
1783
2351
  if (samlIdentityBoundaryChanged(currentSamlConfig, updatedSamlConfig)) providerIdentityBoundaryChanged = true;
1784
2352
  updateData.samlConfig = JSON.stringify(updatedSamlConfig);
1785
2353
  }
@@ -1797,6 +2365,10 @@ const updateSSOProvider = (options) => {
1797
2365
  if (oidcIdentityBoundaryChanged(currentOidcConfig, updatedOidcConfig)) providerIdentityBoundaryChanged = true;
1798
2366
  updateData.oidcConfig = JSON.stringify(updatedOidcConfig);
1799
2367
  }
2368
+ await guardSSOProviderMutation(options, {
2369
+ action: "update",
2370
+ isAuthenticationBoundaryChange: providerIdentityBoundaryChanged
2371
+ }, existingProvider, trx, ctx.context.logger);
1800
2372
  if (providerIdentityBoundaryChanged) {
1801
2373
  if (await trx.findOne({
1802
2374
  model: "account",
@@ -1827,7 +2399,7 @@ const updateSSOProvider = (options) => {
1827
2399
  return ctx.json(sanitizeProvider(fullProvider, ctx.context.baseURL, options));
1828
2400
  });
1829
2401
  };
1830
- const deleteSSOProvider = () => {
2402
+ const deleteSSOProvider = (options) => {
1831
2403
  return createAuthEndpoint("/sso/delete-provider", {
1832
2404
  method: "POST",
1833
2405
  use: [sessionMiddleware],
@@ -1844,9 +2416,16 @@ const deleteSSOProvider = () => {
1844
2416
  } }
1845
2417
  }, async (ctx) => {
1846
2418
  const { providerId } = ctx.body;
1847
- await checkProviderAccess(ctx, providerId);
2419
+ const authorizedProvider = await checkProviderAccess(ctx, providerId);
2420
+ await assertProviderMutationGuardCapabilities(options, ctx.context.adapter);
1848
2421
  await runWithTransaction(ctx.context.adapter, async () => {
1849
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);
1850
2429
  await trx.deleteMany({
1851
2430
  model: "account",
1852
2431
  where: [{
@@ -1857,6 +2436,9 @@ const deleteSSOProvider = () => {
1857
2436
  await trx.delete({
1858
2437
  model: "ssoProvider",
1859
2438
  where: [{
2439
+ field: "id",
2440
+ value: existingProvider.id
2441
+ }, {
1860
2442
  field: "providerId",
1861
2443
  value: providerId
1862
2444
  }]
@@ -1987,7 +2569,7 @@ const verifyDomain = (options) => {
1987
2569
  };
1988
2570
  //#endregion
1989
2571
  //#region src/saml-state.ts
1990
- async function generateRelayState(c, link) {
2572
+ async function generateRelayState(c, link, providerReference) {
1991
2573
  const callbackURL = c.body.callbackURL;
1992
2574
  if (!callbackURL) throw new APIError("BAD_REQUEST", { message: "callbackURL is required" });
1993
2575
  const stateData = {
@@ -1996,8 +2578,12 @@ async function generateRelayState(c, link) {
1996
2578
  errorURL: c.body.errorCallbackURL,
1997
2579
  newUserURL: c.body.newUserCallbackURL,
1998
2580
  link,
2581
+ /**
2582
+ * This is the actual expiry time of the state
2583
+ */
1999
2584
  expiresAt: Date.now() + 600 * 1e3,
2000
- requestSignUp: c.body.requestSignUp
2585
+ requestSignUp: c.body.requestSignUp,
2586
+ serverContext: providerReference ? { [SSO_PROVIDER_STATE_KEY]: providerReference } : void 0
2001
2587
  };
2002
2588
  try {
2003
2589
  return generateGenericState(c, stateData, { cookieName: "relay_state" });
@@ -2016,6 +2602,10 @@ async function parseRelayState(c) {
2016
2602
  try {
2017
2603
  parsedData = await parseGenericState(c, state, {
2018
2604
  cookieName: "relay_state",
2605
+ /**
2606
+ * SAML ACS receives a POST from the IdP, which is typically cross-origin.
2607
+ * SameSite=Lax (default) cookies are not sent on cross-site POST requests.
2608
+ */
2019
2609
  skipStateCookieCheck: true
2020
2610
  });
2021
2611
  } catch (error) {
@@ -2028,122 +2618,6 @@ async function parseRelayState(c) {
2028
2618
  if (!parsedData.errorURL) parsedData.errorURL = errorURL;
2029
2619
  return parsedData;
2030
2620
  }
2031
- const saml = typeof samlifyNamespace.SPMetadata === "function" && typeof samlifyNamespace.setSchemaValidator === "function" ? samlifyNamespace : samlifyDefault ?? samlifyNamespace;
2032
- //#endregion
2033
- //#region src/routes/helpers.ts
2034
- /**
2035
- * Same as `normalizePem`, but applied across the resolved list of IdP signing
2036
- * certificates so multi-cert rotation configs survive the line-trim step.
2037
- */
2038
- function normalizePemList(certs) {
2039
- if (!certs) return certs;
2040
- return certs.map((pem) => normalizePem(pem) ?? pem);
2041
- }
2042
- async function findSAMLProvider(providerId, options, adapter) {
2043
- if (options?.defaultSSO?.length) {
2044
- const match = options.defaultSSO.find((p) => p.providerId === providerId);
2045
- if (match) return {
2046
- ...match,
2047
- userId: "default",
2048
- issuer: match.samlConfig?.issuer || "",
2049
- ...options.domainVerification?.enabled ? { domainVerified: true } : {}
2050
- };
2051
- }
2052
- const res = await adapter.findOne({
2053
- model: "ssoProvider",
2054
- where: [{
2055
- field: "providerId",
2056
- value: providerId
2057
- }]
2058
- });
2059
- if (!res) return null;
2060
- return {
2061
- ...res,
2062
- samlConfig: res.samlConfig ? safeJsonParse(res.samlConfig) || void 0 : void 0
2063
- };
2064
- }
2065
- function createSP(config, baseURL, providerId, opts) {
2066
- const spData = config.spMetadata;
2067
- const sloLocation = `${baseURL}/sso/saml2/sp/slo/${providerId}`;
2068
- const acsUrl = `${baseURL}/sso/saml2/sp/acs/${providerId}`;
2069
- let metadata = spData?.metadata;
2070
- if (!metadata) metadata = saml.SPMetadata({
2071
- entityID: spData?.entityID || config.issuer,
2072
- assertionConsumerService: [{
2073
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
2074
- Location: acsUrl
2075
- }],
2076
- singleLogoutService: opts?.sloOptions ? [{
2077
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
2078
- Location: sloLocation
2079
- }, {
2080
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
2081
- Location: sloLocation
2082
- }] : void 0,
2083
- wantMessageSigned: config.wantAssertionsSigned || false,
2084
- authnRequestsSigned: config.authnRequestsSigned || false,
2085
- nameIDFormat: config.identifierFormat ? [config.identifierFormat] : void 0
2086
- }).getMetadata() || "";
2087
- return saml.ServiceProvider({
2088
- metadata,
2089
- allowCreate: true,
2090
- wantLogoutRequestSigned: opts?.sloOptions?.wantLogoutRequestSigned ?? false,
2091
- wantLogoutResponseSigned: opts?.sloOptions?.wantLogoutResponseSigned ?? false,
2092
- privateKey: normalizePem(spData?.privateKey || config.privateKey),
2093
- privateKeyPass: spData?.privateKeyPass,
2094
- isAssertionEncrypted: spData?.isAssertionEncrypted || false,
2095
- encPrivateKey: normalizePem(spData?.encPrivateKey),
2096
- encPrivateKeyPass: spData?.encPrivateKeyPass,
2097
- relayState: opts?.relayState,
2098
- clockDrifts: opts?.clockSkew && opts?.clockSkew !== 0 ? [-opts.clockSkew, opts.clockSkew] : void 0
2099
- });
2100
- }
2101
- function createIdP(config) {
2102
- const idpData = config.idpMetadata;
2103
- if (idpData?.metadata) return saml.IdentityProvider({
2104
- metadata: idpData.metadata,
2105
- privateKey: normalizePem(idpData.privateKey),
2106
- privateKeyPass: idpData.privateKeyPass,
2107
- isAssertionEncrypted: idpData.isAssertionEncrypted,
2108
- encPrivateKey: normalizePem(idpData.encPrivateKey),
2109
- encPrivateKeyPass: idpData.encPrivateKeyPass
2110
- });
2111
- return saml.IdentityProvider({
2112
- entityID: idpData?.entityID || config.issuer,
2113
- singleSignOnService: idpData?.singleSignOnService || [{
2114
- Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
2115
- Location: config.entryPoint
2116
- }],
2117
- singleLogoutService: idpData?.singleLogoutService,
2118
- signingCert: normalizePemList(resolveSigningCerts(config)),
2119
- wantAuthnRequestsSigned: config.authnRequestsSigned || false,
2120
- isAssertionEncrypted: idpData?.isAssertionEncrypted || false,
2121
- encPrivateKey: normalizePem(idpData?.encPrivateKey),
2122
- encPrivateKeyPass: idpData?.encPrivateKeyPass
2123
- });
2124
- }
2125
- function escapeHtml(str) {
2126
- if (!str) return "";
2127
- return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2128
- }
2129
- function isSAMLPostBindingLocation(value) {
2130
- let url;
2131
- try {
2132
- url = new URL(value);
2133
- } catch {
2134
- return false;
2135
- }
2136
- return url.protocol === "http:" || url.protocol === "https:";
2137
- }
2138
- function createSAMLPostForm(action, samlParam, samlValue, relayState) {
2139
- if (!isSAMLPostBindingLocation(action)) throw new APIError("BAD_REQUEST", { message: "SAML POST binding location must be an absolute http or https URL" });
2140
- const safeAction = escapeHtml(action);
2141
- const safeSamlParam = escapeHtml(samlParam);
2142
- const safeSamlValue = escapeHtml(samlValue);
2143
- const safeRelayState = relayState ? escapeHtml(relayState) : void 0;
2144
- const html = `<!DOCTYPE html><html><body onload="document.forms[0].submit();"><form method="POST" action="${safeAction}"><input type="hidden" name="${safeSamlParam}" value="${safeSamlValue}" />${safeRelayState ? `<input type="hidden" name="RelayState" value="${safeRelayState}" />` : ""}<noscript><input type="submit" value="Continue" /></noscript></form></body></html>`;
2145
- return new Response(html, { headers: { "Content-Type": "text/html" } });
2146
- }
2147
2621
  //#endregion
2148
2622
  //#region src/saml/timestamp.ts
2149
2623
  /**
@@ -2187,42 +2661,47 @@ function validateSAMLTimestamp(conditions, options = {}) {
2187
2661
  }
2188
2662
  //#endregion
2189
2663
  //#region src/routes/saml-pipeline.ts
2190
- /**
2191
- * Validates and returns a safe redirect URL.
2192
- * - Prevents open redirect attacks by validating against trusted origins
2193
- * - Prevents redirect loops by checking if URL points to callback route
2194
- * - Falls back to appOrigin if URL is invalid or unsafe
2195
- */
2196
- function getSafeRedirectUrl(url, callbackPath, appOrigin, isTrustedOrigin) {
2197
- if (!url) return appOrigin;
2198
- if (url.startsWith("/") && !url.startsWith("//")) {
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
+ }
2668
+ function getSafeRedirectCandidate(url, callbackPathname, appOrigin, isTrustedOrigin) {
2669
+ if (!url) return;
2670
+ if (url.startsWith("/")) {
2671
+ if (!isSafeSAMLRedirectPath(url)) return;
2199
2672
  try {
2200
2673
  const absoluteUrl = new URL(url, appOrigin);
2201
- if (absoluteUrl.origin !== appOrigin) return appOrigin;
2202
- const callbackPathname = new URL(callbackPath).pathname;
2203
- if (absoluteUrl.pathname === callbackPathname) return appOrigin;
2674
+ if (absoluteUrl.origin !== appOrigin || absoluteUrl.pathname === callbackPathname) return;
2675
+ return url;
2204
2676
  } catch {
2205
- return appOrigin;
2677
+ return;
2206
2678
  }
2207
- return url;
2208
2679
  }
2680
+ let absoluteUrl;
2209
2681
  try {
2210
- const absoluteUrl = new URL(url);
2211
- if (absoluteUrl.origin === appOrigin) {
2212
- const callbackPathname = new URL(callbackPath).pathname;
2213
- if (absoluteUrl.pathname === callbackPathname) return appOrigin;
2214
- return url;
2215
- }
2216
- } catch {}
2217
- if (!isTrustedOrigin(url, { allowRelativePaths: false })) return appOrigin;
2218
- try {
2219
- const callbackPathname = new URL(callbackPath).pathname;
2220
- if (new URL(url).pathname === callbackPathname) return appOrigin;
2682
+ absoluteUrl = new URL(url);
2221
2683
  } catch {
2222
- if (url === callbackPath || url.startsWith(`${callbackPath}?`)) return appOrigin;
2684
+ return;
2223
2685
  }
2686
+ if (absoluteUrl.origin !== appOrigin && !isTrustedOrigin(url, { allowRelativePaths: false })) return;
2687
+ if (absoluteUrl.origin === appOrigin && absoluteUrl.pathname === callbackPathname) return;
2224
2688
  return url;
2225
2689
  }
2690
+ /**
2691
+ * Returns the first safe redirect URL from an ordered list of candidates.
2692
+ * - Prevents open redirect attacks by validating against trusted origins
2693
+ * - Prevents redirect loops by checking if URL points to callback route
2694
+ * - Tries the next candidate when a URL is invalid or unsafe
2695
+ * - Falls back to appOrigin when no candidate is safe
2696
+ */
2697
+ function getSafeRedirectUrl(candidates, callbackPath, appOrigin, isTrustedOrigin) {
2698
+ const callbackPathname = new URL(callbackPath).pathname;
2699
+ for (const candidate of candidates) {
2700
+ const safeCandidate = getSafeRedirectCandidate(candidate, callbackPathname, appOrigin, isTrustedOrigin);
2701
+ if (safeCandidate) return safeCandidate;
2702
+ }
2703
+ return appOrigin;
2704
+ }
2226
2705
  function buildSAMLRedirectUrl(url, params) {
2227
2706
  const searchParams = new URLSearchParams(params);
2228
2707
  try {
@@ -2238,6 +2717,14 @@ function buildSAMLRedirectUrl(url, params) {
2238
2717
  return `${urlWithoutFragment}${urlWithoutFragment.includes("?") ? "&" : "?"}${searchParams.toString()}${fragment}`;
2239
2718
  }
2240
2719
  }
2720
+ function getSAMLRedirectCandidates(relayStateCallbackUrl, samlConfig, samlOptions) {
2721
+ return [
2722
+ relayStateCallbackUrl,
2723
+ samlConfig?.idpInitiatedCallbackUrl,
2724
+ samlOptions?.idpInitiatedCallbackUrl,
2725
+ samlConfig?.callbackUrl
2726
+ ];
2727
+ }
2241
2728
  function toArray(value) {
2242
2729
  if (Array.isArray(value)) return value;
2243
2730
  return value ? [value] : [];
@@ -2292,10 +2779,11 @@ async function processSAMLResponse(ctx, params, options) {
2292
2779
  if (new TextEncoder().encode(params.SAMLResponse).length > maxResponseSize) throw new APIError("BAD_REQUEST", { message: `SAML response exceeds maximum allowed size (${maxResponseSize} bytes)` });
2293
2780
  const SAMLResponse = params.SAMLResponse.replace(/\s+/g, "");
2294
2781
  let relayState = null;
2782
+ let relayStateValidationFailed = false;
2295
2783
  if (params.RelayState) try {
2296
2784
  relayState = await parseRelayState(ctx);
2297
2785
  } catch {
2298
- relayState = null;
2786
+ relayStateValidationFailed = true;
2299
2787
  }
2300
2788
  const provider = await findSAMLProvider(providerId, options, ctx.context.adapter);
2301
2789
  if (!provider?.samlConfig) throw new APIError("NOT_FOUND", { message: "No SAML provider found" });
@@ -2304,7 +2792,11 @@ async function processSAMLResponse(ctx, params, options) {
2304
2792
  if (!parsedSamlConfig) throw new APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
2305
2793
  const sp = createSP(parsedSamlConfig, ctx.context.baseURL, providerId, { clockSkew: options?.saml?.clockSkew });
2306
2794
  const idp = createIdP(parsedSamlConfig);
2307
- const samlRedirectUrl = getSafeRedirectUrl(relayState?.callbackURL || parsedSamlConfig.callbackUrl, params.currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
2795
+ const redirectCandidates = getSAMLRedirectCandidates(relayState?.callbackURL, parsedSamlConfig, options?.saml);
2796
+ const samlRedirectUrl = getSafeRedirectUrl(redirectCandidates, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
2797
+ const samlErrorRedirectUrl = getSafeRedirectUrl([relayState?.errorURL, samlRedirectUrl], currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
2798
+ params.onErrorRedirectResolved?.(samlErrorRedirectUrl);
2799
+ const stateProviderReference = parseSSOProviderReference(relayState?.serverContext?.[SSO_PROVIDER_STATE_KEY]);
2308
2800
  validateSingleAssertion(SAMLResponse);
2309
2801
  let parsedResponse;
2310
2802
  try {
@@ -2313,15 +2805,9 @@ async function processSAMLResponse(ctx, params, options) {
2313
2805
  RelayState: params.RelayState || void 0
2314
2806
  } });
2315
2807
  if (!parsedResponse?.extract) throw new Error("Invalid SAML response structure");
2316
- } catch (error) {
2317
- ctx.context.logger.error("SAML response validation failed", {
2318
- error,
2319
- samlResponsePreview: SAMLResponse.slice(0, 200)
2320
- });
2321
- throw new APIError("BAD_REQUEST", {
2322
- message: "Invalid SAML response",
2323
- details: error instanceof Error ? error.message : String(error)
2324
- });
2808
+ } catch {
2809
+ ctx.context.logger.error("SAML response validation failed");
2810
+ throw new APIError("BAD_REQUEST", { message: "Invalid SAML response" });
2325
2811
  }
2326
2812
  const { extract } = parsedResponse;
2327
2813
  const samlContent = parsedResponse.samlContent;
@@ -2334,9 +2820,14 @@ async function processSAMLResponse(ctx, params, options) {
2334
2820
  const expectedAudiences = [sp.entityMeta.getEntityID(), parsedSamlConfig.audience];
2335
2821
  const assertionConsumerServiceUrl = sp.entityMeta.getAssertionConsumerService(SAML_HTTP_POST_BINDING);
2336
2822
  const expectedRecipients = getExpectedSAMLRecipients(parsedSamlConfig, ctx.context.baseURL, providerId, currentCallbackPath, assertionConsumerServiceUrl);
2823
+ const serviceProviderPolicy = deriveSAMLServiceProviderPolicy(parsedSamlConfig);
2337
2824
  let samlBindingContent;
2338
2825
  try {
2339
2826
  samlBindingContent = await getSAMLResponseBindingContent(sp, samlContent);
2827
+ if (serviceProviderPolicy.wantAssertionsSigned) verifySAMLAssertionSignature(samlBindingContent, {
2828
+ metadata: idp.entityMeta,
2829
+ signatureAlgorithm: idp.entitySetting.requestSignatureAlgorithm
2830
+ });
2340
2831
  validateSAMLResponseBinding(samlBindingContent, {
2341
2832
  expectedAudiences,
2342
2833
  expectedRecipients
@@ -2345,44 +2836,54 @@ async function processSAMLResponse(ctx, params, options) {
2345
2836
  if (isAPIError(error)) {
2346
2837
  ctx.context.logger.error("SAML response binding validation failed", {
2347
2838
  providerId,
2348
- code: error.body?.code,
2349
- expectedAudiences: expectedAudiences.filter(Boolean),
2350
- expectedRecipients: expectedRecipients.filter(Boolean)
2839
+ code: error.body?.code
2351
2840
  });
2352
- throw ctx.redirect(buildSAMLRedirectUrl(samlRedirectUrl, {
2841
+ throw ctx.redirect(buildSAMLRedirectUrl(samlErrorRedirectUrl, {
2353
2842
  error: "invalid_saml_response",
2354
2843
  error_description: error.body?.message || error.message || "Invalid SAML response"
2355
2844
  }));
2356
2845
  }
2357
- ctx.context.logger.error("SAML response binding validation failed", {
2358
- providerId,
2359
- error,
2360
- expectedAudiences: expectedAudiences.filter(Boolean),
2361
- expectedRecipients: expectedRecipients.filter(Boolean)
2362
- });
2363
- throw ctx.redirect(buildSAMLRedirectUrl(samlRedirectUrl, {
2846
+ ctx.context.logger.error("SAML response binding validation failed", { providerId });
2847
+ throw ctx.redirect(buildSAMLRedirectUrl(samlErrorRedirectUrl, {
2364
2848
  error: "invalid_saml_response",
2365
2849
  error_description: "SAML response binding could not be validated"
2366
2850
  }));
2367
2851
  }
2368
- await validateInResponseTo(ctx, {
2852
+ const requestProviderReference = (await validateInResponseTo(ctx, {
2369
2853
  extract,
2370
2854
  providerId,
2371
2855
  options: {
2372
2856
  enableInResponseToValidation: options?.saml?.enableInResponseToValidation,
2373
2857
  allowIdpInitiated: options?.saml?.allowIdpInitiated
2374
2858
  },
2375
- redirectUrl: samlRedirectUrl
2376
- });
2859
+ redirectUrl: samlErrorRedirectUrl
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
+ }));
2377
2878
  validateAudience(ctx, {
2378
2879
  extract,
2379
2880
  expectedAudience: parsedSamlConfig.audience || sp.entityMeta.getEntityID(),
2380
2881
  providerId,
2381
- redirectUrl: samlRedirectUrl
2882
+ redirectUrl: samlErrorRedirectUrl
2382
2883
  });
2884
+ const issuer = idp.entityMeta.getEntityID();
2383
2885
  const assertionId = extractAssertionId(samlBindingContent);
2384
2886
  if (assertionId) {
2385
- const issuer = idp.entityMeta.getEntityID();
2386
2887
  const conditions = extract.conditions;
2387
2888
  const clockSkew = options?.saml?.clockSkew ?? 3e5;
2388
2889
  const expiresAt = conditions?.notOnOrAfter ? new Date(conditions.notOnOrAfter).getTime() + clockSkew : Date.now() + DEFAULT_ASSERTION_TTL_MS;
@@ -2402,13 +2903,16 @@ async function processSAMLResponse(ctx, params, options) {
2402
2903
  issuer,
2403
2904
  providerId
2404
2905
  });
2405
- throw ctx.redirect(buildSAMLRedirectUrl(samlRedirectUrl, {
2906
+ throw ctx.redirect(buildSAMLRedirectUrl(samlErrorRedirectUrl, {
2406
2907
  error: "replay_detected",
2407
2908
  error_description: "SAML assertion has already been used"
2408
2909
  }));
2409
2910
  }
2410
2911
  } else ctx.context.logger.warn("Could not extract assertion ID for replay protection", { providerId });
2411
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;
2412
2916
  const mapping = parsedSamlConfig.mapping ?? {};
2413
2917
  const attr = (key) => {
2414
2918
  const value = attributes[key];
@@ -2416,37 +2920,70 @@ async function processSAMLResponse(ctx, params, options) {
2416
2920
  };
2417
2921
  const userInfo = {
2418
2922
  ...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, attributes[value]])),
2419
- id: attr(mapping.id || "nameID") || extract.nameID,
2923
+ id: extract.nameID,
2420
2924
  email: (attr(mapping.email || "email") || extract.nameID || "").toLowerCase(),
2421
2925
  name: [attr(mapping.firstName || "givenName"), attr(mapping.lastName || "surname")].filter(Boolean).join(" ") || attr(mapping.name || "displayName") || extract.nameID,
2422
2926
  emailVerified: options?.trustEmailVerified && mapping.emailVerified ? parseProviderEmailVerified(attr(mapping.emailVerified)) : false
2423
2927
  };
2424
2928
  if (!userInfo.id || !userInfo.email) {
2425
2929
  ctx.context.logger.error("Missing essential user info from SAML response", {
2426
- attributes: Object.keys(attributes),
2427
- mapping,
2428
- extractedId: userInfo.id,
2429
- extractedEmail: userInfo.email
2930
+ providerId,
2931
+ attributeNames: Object.keys(attributes),
2932
+ hasNameId: Boolean(userInfo.id),
2933
+ hasEmail: Boolean(userInfo.email)
2430
2934
  });
2431
2935
  throw new APIError("BAD_REQUEST", { message: "Unable to extract user ID or email from SAML response" });
2432
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
+ };
2433
2949
  const isTrustedProvider = "domainVerified" in provider && !!provider.domainVerified && validateEmailDomain(userInfo.email, provider.domain);
2434
- const callbackUrl = relayState?.callbackURL || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
2435
- const errorUrl = relayState?.errorURL || samlRedirectUrl;
2950
+ const callbackUrl = redirectCandidates.some(Boolean) ? samlRedirectUrl : ctx.context.baseURL;
2951
+ const errorUrl = samlErrorRedirectUrl;
2436
2952
  let result;
2437
2953
  try {
2954
+ if (options?.resolveUser) {
2955
+ assertSSOUserResolutionNativeTransactionSupport(ctx.context.adapter);
2956
+ assertSSOUserResolutionSessionStorage(ctx.context.options);
2957
+ await assertSSOUserResolutionAsyncContextSupport();
2958
+ }
2438
2959
  result = await runWithTransaction(ctx.context.adapter, async () => {
2439
2960
  await lockSSOProviderForAccountLink(ctx, provider);
2440
- 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, {
2441
2979
  userInfo: {
2442
- email: userInfo.email,
2443
- name: userInfo.name || userInfo.email,
2444
- id: userInfo.id,
2445
- emailVerified: userInfo.emailVerified
2980
+ ...providerUser,
2981
+ id: userInfo.id
2446
2982
  },
2447
2983
  account: {
2448
2984
  providerId,
2449
- accountId: userInfo.id,
2985
+ issuer: accountKey.issuer,
2986
+ providerAccountId: accountKey.providerAccountId,
2450
2987
  accessToken: "",
2451
2988
  refreshToken: ""
2452
2989
  },
@@ -2460,15 +2997,26 @@ async function processSAMLResponse(ctx, params, options) {
2460
2997
  }
2461
2998
  },
2462
2999
  isTrustedProvider,
2463
- 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
2464
3007
  });
2465
- });
3008
+ return options?.resolveUser ? requireSuccessfulSSOAuthentication(authentication) : authentication;
3009
+ }, { onAfterCommitHookError() {
3010
+ ctx.context.logger.error("Committed SSO authentication after-hook failed");
3011
+ } });
2466
3012
  } catch (e) {
2467
- 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, {
2468
3016
  error: e.body.code,
2469
3017
  ...e.body.message ? { error_description: e.body.message } : {}
2470
3018
  }));
2471
- throw e;
3019
+ else throw e;
2472
3020
  }
2473
3021
  if (result.error) throw ctx.redirect(buildSAMLRedirectUrl(callbackUrl, { error: result.error.split(" ").join("_") }));
2474
3022
  const { session, user } = result.data;
@@ -2482,7 +3030,7 @@ async function processSAMLResponse(ctx, params, options) {
2482
3030
  profile: {
2483
3031
  providerType: "saml",
2484
3032
  providerId,
2485
- accountId: userInfo.id,
3033
+ providerAccountId: userInfo.id,
2486
3034
  email: userInfo.email,
2487
3035
  emailVerified: userInfo.emailVerified,
2488
3036
  rawAttributes: attributes
@@ -2490,6 +3038,7 @@ async function processSAMLResponse(ctx, params, options) {
2490
3038
  provider,
2491
3039
  provisioningOptions: options?.organizationProvisioning
2492
3040
  });
3041
+ if ("accountCookie" in result && result.accountCookie) await setAccountCookie(ctx, result.accountCookie);
2493
3042
  await setSessionCookie(ctx, {
2494
3043
  session,
2495
3044
  user
@@ -2514,7 +3063,7 @@ async function processSAMLResponse(ctx, params, options) {
2514
3063
  expiresAt: session.expiresAt
2515
3064
  }).catch((e) => ctx.context.logger.warn("Failed to create SAML session lookup record", e));
2516
3065
  }
2517
- return getSafeRedirectUrl(relayState?.callbackURL || parsedSamlConfig.callbackUrl, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
3066
+ return samlRedirectUrl;
2518
3067
  }
2519
3068
  //#endregion
2520
3069
  //#region src/routes/sso.ts
@@ -2660,10 +3209,6 @@ const registerSSOProvider = (options) => {
2660
3209
  type: "object",
2661
3210
  nullable: true,
2662
3211
  properties: {
2663
- id: {
2664
- type: "string",
2665
- description: "Field mapping for user ID (defaults to 'sub')"
2666
- },
2667
3212
  email: {
2668
3213
  type: "string",
2669
3214
  description: "Field mapping for email (defaults to 'email')"
@@ -2689,11 +3234,7 @@ const registerSSOProvider = (options) => {
2689
3234
  description: "Additional field mappings"
2690
3235
  }
2691
3236
  },
2692
- required: [
2693
- "id",
2694
- "email",
2695
- "name"
2696
- ]
3237
+ required: ["email", "name"]
2697
3238
  }
2698
3239
  },
2699
3240
  required: [
@@ -2750,9 +3291,10 @@ const registerSSOProvider = (options) => {
2750
3291
  })).length >= limit) throw new APIError("FORBIDDEN", { message: "You have reached the maximum number of SSO providers" });
2751
3292
  const body = ctx.body;
2752
3293
  const additionalFields = parseSSOProviderAdditionalFields(options, body, "create");
2753
- if (body.samlConfig?.idpMetadata?.metadata) {
3294
+ if (body.samlConfig) {
2754
3295
  const maxMetadataSize = options?.saml?.maxMetadataSize ?? 102400;
2755
- 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);
2756
3298
  }
2757
3299
  if (ctx.body.organizationId) {
2758
3300
  const member = await ctx.context.adapter.findOne({
@@ -2768,28 +3310,16 @@ const registerSSOProvider = (options) => {
2768
3310
  if (!member) throw new APIError("BAD_REQUEST", { message: "You are not a member of the organization" });
2769
3311
  if (ctx.context.hasPlugin("organization") && !hasOrgAdminRole(member)) throw new APIError("FORBIDDEN", { message: "You must be an organization owner or admin to register SSO providers" });
2770
3312
  }
2771
- if (new Set([
3313
+ if ((/* @__PURE__ */ new Set([
2772
3314
  ...BUILT_IN_ACCOUNT_PROVIDER_IDS,
2773
3315
  ...Object.keys(ctx.context.options.socialProviders ?? {}),
2774
3316
  ...ctx.context.socialProviders.map((p) => p.id),
2775
3317
  ...ctx.context.trustedProviders,
2776
3318
  ...options?.defaultSSO?.map((p) => p.providerId) ?? []
2777
- ]).has(body.providerId)) {
3319
+ ])).has(body.providerId)) {
2778
3320
  ctx.context.logger.warn(`SSO provider registration rejected for reserved providerId: ${body.providerId}`);
2779
3321
  throw new APIError("UNPROCESSABLE_ENTITY", { message: "This providerId is reserved and cannot be used for an SSO provider" });
2780
3322
  }
2781
- if (ctx.context.hasPlugin("scim")) {
2782
- if (await ctx.context.adapter.findOne({
2783
- model: "scimProvider",
2784
- where: [{
2785
- field: "providerId",
2786
- value: body.providerId
2787
- }]
2788
- })) {
2789
- ctx.context.logger.warn(`SSO provider registration rejected for SCIM providerId: ${body.providerId}`);
2790
- throw new APIError("UNPROCESSABLE_ENTITY", { message: "This providerId is already used by a SCIM provider and cannot be used for an SSO provider" });
2791
- }
2792
- }
2793
3323
  if (await ctx.context.adapter.findOne({
2794
3324
  model: "ssoProvider",
2795
3325
  where: [{
@@ -2868,6 +3398,11 @@ const registerSSOProvider = (options) => {
2868
3398
  digestAlgorithm: body.samlConfig.digestAlgorithm
2869
3399
  }, options?.saml?.algorithms);
2870
3400
  validateCertSources(body.samlConfig);
3401
+ assertSAMLIdentityProviderAuthority(body.samlConfig);
3402
+ assertSAMLServiceProviderMetadataPolicy({
3403
+ ...body.samlConfig,
3404
+ issuer: body.issuer
3405
+ });
2871
3406
  const hasIdpMetadata = body.samlConfig.idpMetadata?.metadata;
2872
3407
  let hasEntryPoint = false;
2873
3408
  if (body.samlConfig.entryPoint) try {
@@ -2899,6 +3434,7 @@ const registerSSOProvider = (options) => {
2899
3434
  cert: body.samlConfig.cert,
2900
3435
  audience: body.samlConfig.audience,
2901
3436
  callbackUrl: body.samlConfig.callbackUrl,
3437
+ idpInitiatedCallbackUrl: body.samlConfig.idpInitiatedCallbackUrl,
2902
3438
  idpMetadata: body.samlConfig.idpMetadata,
2903
3439
  spMetadata: body.samlConfig.spMetadata,
2904
3440
  wantAssertionsSigned: body.samlConfig.wantAssertionsSigned,
@@ -3107,7 +3643,7 @@ const signInSSO = (options) => {
3107
3643
  throw error;
3108
3644
  }
3109
3645
  if (!config.authorizationEndpoint) throw new APIError("BAD_REQUEST", { message: "Invalid OIDC configuration. Authorization URL not found." });
3110
- if (options?.redirectURI?.trim()) await addOAuthServerContext({ ssoProviderId: provider.providerId });
3646
+ await addOAuthServerContext({ [SSO_PROVIDER_STATE_KEY]: await computeSSOProviderReference(provider) });
3111
3647
  const state = await generateState(ctx);
3112
3648
  const redirectURI = getOIDCRedirectURI(ctx.context.baseURL, provider.providerId, options);
3113
3649
  const authorizationURL = await createAuthorizationURL({
@@ -3139,7 +3675,8 @@ const signInSSO = (options) => {
3139
3675
  const parsedSamlConfig = typeof provider.samlConfig === "object" ? provider.samlConfig : safeJsonParse(provider.samlConfig);
3140
3676
  if (!parsedSamlConfig) throw new APIError("BAD_REQUEST", { message: "Invalid SAML configuration" });
3141
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" });
3142
- const { state: relayState } = await generateRelayState(ctx, void 0);
3678
+ const providerReference = await computeSSOProviderReference(provider);
3679
+ const { state: relayState } = await generateRelayState(ctx, void 0, providerReference);
3143
3680
  const sp = createSP(parsedSamlConfig, ctx.context.baseURL, provider.providerId, { relayState });
3144
3681
  const idp = createIdP(parsedSamlConfig);
3145
3682
  const loginRequest = sp.createLoginRequest(idp, "redirect");
@@ -3149,6 +3686,7 @@ const signInSSO = (options) => {
3149
3686
  const record = {
3150
3687
  id: loginRequest.id,
3151
3688
  providerId: provider.providerId,
3689
+ providerReference,
3152
3690
  createdAt: Date.now(),
3153
3691
  expiresAt: Date.now() + ttl
3154
3692
  };
@@ -3196,13 +3734,14 @@ function getOIDCErrorDescription(error, fallback) {
3196
3734
  * @param stateData - Pre-parsed state data. If not provided, it will be
3197
3735
  * parsed from the request context.
3198
3736
  */
3199
- async function handleOIDCCallback(ctx, options, providerId, stateData) {
3737
+ async function handleOIDCCallback(ctx, options, providerId, stateData, parsedProviderReference) {
3200
3738
  const { code, error, error_description } = ctx.query;
3201
3739
  if (!stateData) stateData = await parseState(ctx);
3202
3740
  if (!stateData) {
3203
3741
  const errorURL = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
3204
3742
  throw ctx.redirect(`${errorURL}?error=invalid_state`);
3205
3743
  }
3744
+ const providerReference = parsedProviderReference ?? parseSSOProviderReference(stateData.serverContext?.["ssoProviderReference"]);
3206
3745
  const { callbackURL, errorURL, newUserURL, requestSignUp } = stateData;
3207
3746
  const redirectOIDCError = (error, description) => {
3208
3747
  const baseURL = errorURL || callbackURL;
@@ -3216,6 +3755,8 @@ async function handleOIDCCallback(ctx, options, providerId, stateData) {
3216
3755
  if (!code || error) redirectOIDCError(error || "invalid_request", error_description || (error ? error : "authorization_code_not_found"));
3217
3756
  const provider = await resolveOIDCProvider(ctx, options, providerId);
3218
3757
  if (!provider) throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=provider not found`);
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");
3219
3760
  if (options?.domainVerification?.enabled && !("domainVerified" in provider && provider.domainVerified)) throw new APIError("UNAUTHORIZED", { message: "Provider domain has not been verified" });
3220
3761
  let config = provider.oidcConfig;
3221
3762
  if (!config) throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=provider not found`);
@@ -3287,7 +3828,28 @@ async function handleOIDCCallback(ctx, options, providerId, stateData) {
3287
3828
  if (!tokenResponse) throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=token_response_not_found`);
3288
3829
  let userInfo = null;
3289
3830
  const mapping = config.mapping || {};
3831
+ const readStringClaim = (claims, claim) => {
3832
+ const value = claims[claim];
3833
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3834
+ };
3290
3835
  let rawProfile;
3836
+ let verifiedIdToken = null;
3837
+ if (tokenResponse.idToken) {
3838
+ const jwksEndpoint = config.jwksEndpoint;
3839
+ if (!jwksEndpoint) redirectOIDCError("invalid_provider", "jwks_endpoint_not_found");
3840
+ const verified = await validateOIDCIdToken(tokenResponse.idToken, jwksEndpoint, {
3841
+ audience: config.clientId,
3842
+ issuer: provider.issuer
3843
+ }, (url) => ctx.context.isTrustedOrigin(url)).catch((error) => {
3844
+ if (error instanceof DiscoveryError) redirectOIDCError("invalid_provider", error.message);
3845
+ ctx.context.logger.error(error);
3846
+ return null;
3847
+ });
3848
+ if (!verified) redirectOIDCError("invalid_provider", "token_not_verified");
3849
+ if (!readStringClaim(verified.payload, "sub")) redirectOIDCError("invalid_provider", "id_token_subject_missing");
3850
+ verifiedIdToken = verified;
3851
+ }
3852
+ if (options?.resolveUser && !verifiedIdToken) redirectOIDCError("invalid_provider", "id_token_required_for_user_resolution");
3291
3853
  if (config.userInfoEndpoint) {
3292
3854
  const userInfoResponse = await fetchOIDCEndpoint("userInfoEndpoint", config.userInfoEndpoint, { headers: { Authorization: `Bearer ${tokenResponse.accessToken}` } }, (url) => ctx.context.isTrustedOrigin(url)).catch((e) => {
3293
3855
  if (e instanceof DiscoveryError) redirectOIDCError("invalid_provider", e.message);
@@ -3295,58 +3857,85 @@ async function handleOIDCCallback(ctx, options, providerId, stateData) {
3295
3857
  });
3296
3858
  if (userInfoResponse.error) redirectOIDCError("invalid_provider", userInfoResponse.error.message || userInfoResponse.error.statusText || "userinfo_response_error");
3297
3859
  const rawUserInfo = userInfoResponse.data ?? redirectOIDCError("invalid_provider", "userinfo_response_not_found");
3860
+ if (verifiedIdToken && rawUserInfo.sub !== verifiedIdToken.payload.sub) redirectOIDCError("invalid_provider", "id_token_userinfo_subject_mismatch");
3298
3861
  rawProfile = rawUserInfo;
3299
3862
  userInfo = {
3300
3863
  ...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, rawUserInfo[value]])),
3301
- id: rawUserInfo[mapping.id || "sub"],
3302
- email: rawUserInfo[mapping.email || "email"],
3864
+ id: readStringClaim(rawUserInfo, "sub"),
3865
+ email: readStringClaim(rawUserInfo, mapping.email || "email"),
3303
3866
  emailVerified: options?.trustEmailVerified ? parseProviderEmailVerified(rawUserInfo[mapping.emailVerified || "email_verified"]) : false,
3304
- name: rawUserInfo[mapping.name || "name"],
3305
- image: rawUserInfo[mapping.image || "picture"]
3867
+ name: readStringClaim(rawUserInfo, mapping.name || "name"),
3868
+ image: readStringClaim(rawUserInfo, mapping.image || "picture")
3306
3869
  };
3307
- } else if (tokenResponse.idToken) {
3308
- const idToken = decodeJwt(tokenResponse.idToken);
3870
+ } else if (verifiedIdToken) {
3871
+ const idToken = verifiedIdToken.payload;
3309
3872
  rawProfile = idToken;
3310
- if (!config.jwksEndpoint) throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=jwks_endpoint_not_found`);
3311
- const verified = await validateOIDCIdToken(tokenResponse.idToken, config.jwksEndpoint, {
3312
- audience: config.clientId,
3313
- issuer: provider.issuer
3314
- }, (url) => ctx.context.isTrustedOrigin(url)).catch((e) => {
3315
- if (e instanceof DiscoveryError) redirectOIDCError("invalid_provider", e.message);
3316
- ctx.context.logger.error(e);
3317
- return null;
3318
- });
3319
- if (!verified) throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=token_not_verified`);
3320
3873
  userInfo = {
3321
- ...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, verified.payload[value]])),
3322
- id: idToken[mapping.id || "sub"],
3323
- email: idToken[mapping.email || "email"],
3874
+ ...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, idToken[value]])),
3875
+ id: idToken.sub,
3876
+ email: readStringClaim(idToken, mapping.email || "email"),
3324
3877
  emailVerified: options?.trustEmailVerified ? parseProviderEmailVerified(idToken[mapping.emailVerified || "email_verified"]) : false,
3325
- name: idToken[mapping.name || "name"],
3326
- image: idToken[mapping.image || "picture"]
3878
+ name: readStringClaim(idToken, mapping.name || "name"),
3879
+ image: readStringClaim(idToken, mapping.image || "picture")
3327
3880
  };
3328
3881
  } else throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=user_info_endpoint_not_found`);
3329
3882
  if (!userInfo.email || !userInfo.id) throw ctx.redirect(`${errorURL || callbackURL}?error=invalid_provider&error_description=missing_user_info`);
3330
3883
  const userInfoEmail = userInfo.email;
3331
3884
  const userInfoId = userInfo.id;
3885
+ const providerUser = {
3886
+ ...Object.fromEntries(Object.entries(userInfo).filter(([key]) => key !== "id")),
3887
+ email: userInfoEmail,
3888
+ name: typeof userInfo.name === "string" ? userInfo.name : "",
3889
+ image: typeof userInfo.image === "string" ? userInfo.image : void 0,
3890
+ emailVerified: options?.trustEmailVerified ? userInfo.emailVerified === true : false
3891
+ };
3892
+ const accountKey = {
3893
+ issuer: verifiedIdToken && readStringClaim(verifiedIdToken.payload, "iss") || provider.issuer,
3894
+ providerAccountId: userInfoId
3895
+ };
3332
3896
  const isTrustedProvider = "domainVerified" in provider && provider.domainVerified === true && validateEmailDomain(userInfoEmail, provider.domain);
3333
3897
  let linked;
3334
3898
  try {
3899
+ if (options?.resolveUser) {
3900
+ assertSSOUserResolutionNativeTransactionSupport(ctx.context.adapter);
3901
+ assertSSOUserResolutionSessionStorage(ctx.context.options);
3902
+ await assertSSOUserResolutionAsyncContextSupport();
3903
+ }
3335
3904
  linked = await runWithTransaction(ctx.context.adapter, async () => {
3336
3905
  await lockSSOProviderForAccountLink(ctx, provider);
3337
- return handleOAuthUserInfo(ctx, {
3906
+ const currentProvider = await resolveOIDCProvider(ctx, options, providerId, await getCurrentAdapter(ctx.context.adapter));
3907
+ if (!currentProvider || !await isCurrentSSOProviderReference(currentProvider, acceptedProviderReference)) throw new APIError("CONFLICT", {
3908
+ code: "SSO_PROVIDER_CHANGED",
3909
+ message: "SSO provider changed while account linking was in progress"
3910
+ });
3911
+ const resolution = options?.resolveUser ? await resolveSSOUser(options.resolveUser, {
3912
+ protocol: "oidc",
3913
+ providerId: provider.providerId,
3914
+ accountKey,
3915
+ providerUser,
3916
+ providerClaims: rawProfile ?? {},
3917
+ verifiedIdTokenClaims: verifiedIdToken?.payload ?? {},
3918
+ providerReference: acceptedProviderReference
3919
+ }, await getCurrentAdapter(ctx.context.adapter), ctx.context.logger) : void 0;
3920
+ if (resolution?.action === "reject") throw new APIError("FORBIDDEN", {
3921
+ code: resolution.code,
3922
+ ...resolution.message === void 0 ? {} : { message: resolution.message }
3923
+ });
3924
+ const authentication = await handleOAuthUserInfo(ctx, {
3338
3925
  userInfo: {
3339
- email: userInfoEmail,
3340
- name: userInfo.name || "",
3926
+ ...providerUser,
3927
+ email: providerUser.email,
3928
+ name: providerUser.name,
3341
3929
  id: userInfoId,
3342
- image: userInfo.image,
3343
- emailVerified: options?.trustEmailVerified ? userInfo.emailVerified || false : false
3930
+ image: providerUser.image,
3931
+ emailVerified: providerUser.emailVerified
3344
3932
  },
3345
3933
  account: {
3346
3934
  idToken: tokenResponse.idToken,
3347
3935
  accessToken: tokenResponse.accessToken,
3348
3936
  refreshToken: tokenResponse.refreshToken,
3349
- accountId: userInfoId,
3937
+ issuer: accountKey.issuer,
3938
+ providerAccountId: userInfoId,
3350
3939
  providerId: provider.providerId,
3351
3940
  accessTokenExpiresAt: tokenResponse.accessTokenExpiresAt,
3352
3941
  refreshTokenExpiresAt: tokenResponse.refreshTokenExpiresAt,
@@ -3363,18 +3952,28 @@ async function handleOIDCCallback(ctx, options, providerId, stateData) {
3363
3952
  }
3364
3953
  },
3365
3954
  isTrustedProvider,
3366
- trustProviderByName: false
3955
+ trustProviderByName: false,
3956
+ selectedUser: resolution?.action === "link" ? {
3957
+ userId: resolution.userId,
3958
+ profile: resolution.profile
3959
+ } : void 0,
3960
+ deferNonDatabaseWrites: !!options?.resolveUser,
3961
+ requireExactAccountBinding: !!options?.resolveUser
3367
3962
  });
3368
- });
3963
+ return options?.resolveUser ? requireSuccessfulSSOAuthentication(authentication) : authentication;
3964
+ }, { onAfterCommitHookError(error) {
3965
+ ctx.context.logger.error("Committed SSO authentication after-hook failed", error);
3966
+ } });
3369
3967
  } catch (e) {
3370
- if (isAPIError(e) && e.body?.code) {
3968
+ const failedAuthentication = getFailedSSOAuthenticationResult(e);
3969
+ if (failedAuthentication) linked = failedAuthentication;
3970
+ else if (isAPIError(e) && e.body?.code) {
3371
3971
  const baseURL = errorURL || callbackURL;
3372
3972
  const params = new URLSearchParams({ error: e.body.code });
3373
3973
  if (e.body.message) params.set("error_description", e.body.message);
3374
3974
  const sep = baseURL.includes("?") ? "&" : "?";
3375
3975
  throw ctx.redirect(`${baseURL}${sep}${params.toString()}`);
3376
- }
3377
- throw e;
3976
+ } else throw e;
3378
3977
  }
3379
3978
  if (linked.error) {
3380
3979
  const baseURL = errorURL || callbackURL;
@@ -3394,7 +3993,7 @@ async function handleOIDCCallback(ctx, options, providerId, stateData) {
3394
3993
  profile: {
3395
3994
  providerType: "oidc",
3396
3995
  providerId: provider.providerId,
3397
- accountId: userInfoId,
3996
+ providerAccountId: userInfoId,
3398
3997
  email: userInfoEmail,
3399
3998
  emailVerified: Boolean(userInfo.emailVerified),
3400
3999
  rawAttributes: userInfo
@@ -3403,6 +4002,7 @@ async function handleOIDCCallback(ctx, options, providerId, stateData) {
3403
4002
  token: tokenResponse,
3404
4003
  provisioningOptions: options?.organizationProvisioning
3405
4004
  });
4005
+ if ("accountCookie" in linked && linked.accountCookie) await setAccountCookie(ctx, linked.accountCookie);
3406
4006
  await setSessionCookie(ctx, {
3407
4007
  session,
3408
4008
  user
@@ -3434,7 +4034,7 @@ const callbackSSOEndpointConfig = {
3434
4034
  * and falling back to the `ssoProvider` table. Returns `null` when no match is
3435
4035
  * found so the caller can decide how to react (redirect, silently skip, etc.).
3436
4036
  */
3437
- async function resolveOIDCProvider(ctx, options, providerId) {
4037
+ async function resolveOIDCProvider(ctx, options, providerId, adapter = ctx.context.adapter) {
3438
4038
  const matchingDefault = options?.defaultSSO?.find((defaultProvider) => defaultProvider.providerId === providerId);
3439
4039
  if (matchingDefault) return {
3440
4040
  ...matchingDefault,
@@ -3442,7 +4042,7 @@ async function resolveOIDCProvider(ctx, options, providerId) {
3442
4042
  userId: "default",
3443
4043
  ...options?.domainVerification?.enabled ? { domainVerified: true } : {}
3444
4044
  };
3445
- return ctx.context.adapter.findOne({
4045
+ return adapter.findOne({
3446
4046
  model: "ssoProvider",
3447
4047
  where: [{
3448
4048
  field: "providerId",
@@ -3482,7 +4082,7 @@ async function bounceIfIdpInitiated(ctx, options, providerId) {
3482
4082
  });
3483
4083
  return;
3484
4084
  }
3485
- if (options?.redirectURI?.trim()) await addOAuthServerContext({ ssoProviderId: provider.providerId });
4085
+ await addOAuthServerContext({ [SSO_PROVIDER_STATE_KEY]: await computeSSOProviderReference(provider) });
3486
4086
  const state = await generateState(ctx);
3487
4087
  const redirectURI = getOIDCRedirectURI(ctx.context.baseURL, provider.providerId, options);
3488
4088
  const authorizationURL = await createAuthorizationURL({
@@ -3534,12 +4134,12 @@ const callbackSSOShared = (options) => {
3534
4134
  const errorURL = ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`;
3535
4135
  throw ctx.redirect(`${errorURL}?error=invalid_state`);
3536
4136
  }
3537
- const providerId = stateData.serverContext?.ssoProviderId;
3538
- if (!providerId) {
4137
+ const providerReference = parseSSOProviderReference(stateData.serverContext?.[SSO_PROVIDER_STATE_KEY]);
4138
+ if (!providerReference) {
3539
4139
  const errorURL = stateData.errorURL || stateData.callbackURL;
3540
- throw ctx.redirect(`${errorURL}?error=invalid_state&error_description=missing_provider_id`);
4140
+ throw ctx.redirect(`${errorURL}?error=invalid_state&error_description=missing_sso_provider_reference`);
3541
4141
  }
3542
- return handleOIDCCallback(ctx, options, providerId, stateData);
4142
+ return handleOIDCCallback(ctx, options, providerReference.providerId, stateData, providerReference);
3543
4143
  });
3544
4144
  };
3545
4145
  const acsEndpointBodySchema = z.object({
@@ -3569,13 +4169,14 @@ const acsEndpoint = (options) => {
3569
4169
  const { providerId } = ctx.params;
3570
4170
  const currentCallbackPath = `${ctx.context.baseURL}/sso/saml2/sp/acs/${providerId}`;
3571
4171
  const appOrigin = new URL(ctx.context.baseURL).origin;
4172
+ let resolvedErrorRedirectUrl;
3572
4173
  if (ctx.method === "GET" && !ctx.body?.SAMLResponse) {
3573
4174
  if (!(await getSessionFromCtx(ctx))?.session) {
3574
4175
  const errorURL = ctx.context.options.onAPIError?.errorURL || `${appOrigin}/error`;
3575
4176
  throw ctx.redirect(`${errorURL}?error=invalid_request`);
3576
4177
  }
3577
4178
  const relayState = ctx.query?.RelayState;
3578
- throw ctx.redirect(getSafeRedirectUrl(relayState, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings)));
4179
+ throw ctx.redirect(getSafeRedirectUrl([relayState], currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings)));
3579
4180
  }
3580
4181
  if (!ctx.body?.SAMLResponse) throw new APIError("BAD_REQUEST", { message: "SAMLResponse is required for POST requests" });
3581
4182
  try {
@@ -3583,15 +4184,33 @@ const acsEndpoint = (options) => {
3583
4184
  SAMLResponse: ctx.body.SAMLResponse,
3584
4185
  RelayState: ctx.body.RelayState,
3585
4186
  providerId,
3586
- currentCallbackPath
4187
+ currentCallbackPath,
4188
+ onErrorRedirectResolved: (url) => {
4189
+ resolvedErrorRedirectUrl = url;
4190
+ }
3587
4191
  }, options);
3588
4192
  throw ctx.redirect(safeRedirectUrl);
3589
4193
  } catch (error) {
3590
4194
  if (error instanceof Response || error && typeof error === "object" && "status" in error && error.status === 302) throw error;
3591
4195
  if (error instanceof APIError && error.statusCode === 400) {
3592
4196
  const errorCode = (error.body?.code || "saml_error").toLowerCase();
3593
- const redirectUrl = getSafeRedirectUrl(ctx.body?.RelayState || void 0, currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
3594
- throw ctx.redirect(`${redirectUrl}${redirectUrl.includes("?") ? "&" : "?"}error=${encodeURIComponent(errorCode)}&error_description=${encodeURIComponent(error.message)}`);
4197
+ let redirectUrl = resolvedErrorRedirectUrl;
4198
+ if (!redirectUrl) {
4199
+ let parsedSamlConfig;
4200
+ try {
4201
+ parsedSamlConfig = (await findSAMLProvider(providerId, options, ctx.context.adapter))?.samlConfig;
4202
+ } catch (providerLookupError) {
4203
+ ctx.context.logger.warn("Failed to resolve SAML provider for error redirect", {
4204
+ providerId,
4205
+ error: providerLookupError
4206
+ });
4207
+ }
4208
+ redirectUrl = getSafeRedirectUrl(getSAMLRedirectCandidates(void 0, parsedSamlConfig, options?.saml), currentCallbackPath, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
4209
+ }
4210
+ throw ctx.redirect(buildSAMLRedirectUrl(redirectUrl, {
4211
+ error: errorCode,
4212
+ error_description: error.message
4213
+ }));
3595
4214
  }
3596
4215
  throw error;
3597
4216
  }
@@ -3620,7 +4239,7 @@ const sloEndpoint = (options) => {
3620
4239
  const samlResponse = ctx.body?.SAMLResponse || ctx.query?.SAMLResponse;
3621
4240
  const relayState = ctx.body?.RelayState || ctx.query?.RelayState;
3622
4241
  const appOrigin = new URL(ctx.context.baseURL).origin;
3623
- const safeErrorURL = getSafeRedirectUrl(relayState, `${appOrigin}/sso/saml2/sp/slo/${providerId}`, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
4242
+ const safeErrorURL = getSafeRedirectUrl([relayState], `${appOrigin}/sso/saml2/sp/slo/${providerId}`, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
3624
4243
  if (!samlRequest && !samlResponse) throw ctx.redirect(`${safeErrorURL}?error=invalid_request&error_description=missing_logout_data`);
3625
4244
  const provider = await findSAMLProvider(providerId, options, ctx.context.adapter);
3626
4245
  if (!provider?.samlConfig) throw APIError.from("NOT_FOUND", SAML_ERROR_CODES.SAML_PROVIDER_NOT_FOUND);
@@ -3660,7 +4279,7 @@ async function handleLogoutResponse(ctx, sp, idp, relayState, providerId) {
3660
4279
  }
3661
4280
  deleteSessionCookie(ctx);
3662
4281
  const appOrigin = new URL(ctx.context.baseURL).origin;
3663
- const safeRedirectUrl = getSafeRedirectUrl(relayState, `${appOrigin}/sso/saml2/sp/slo/${providerId}`, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
4282
+ const safeRedirectUrl = getSafeRedirectUrl([relayState], `${appOrigin}/sso/saml2/sp/slo/${providerId}`, appOrigin, (url, settings) => ctx.context.isTrustedOrigin(url, settings));
3664
4283
  throw ctx.redirect(safeRedirectUrl);
3665
4284
  }
3666
4285
  async function handleLogoutRequest(ctx, sp, idp, relayState, providerId) {
@@ -3814,7 +4433,7 @@ function sso(options) {
3814
4433
  listSSOProviders: listSSOProviders(optionsWithStore),
3815
4434
  getSSOProvider: getSSOProvider(optionsWithStore),
3816
4435
  updateSSOProvider: updateSSOProvider(optionsWithStore),
3817
- deleteSSOProvider: deleteSSOProvider()
4436
+ deleteSSOProvider: deleteSSOProvider(optionsWithStore)
3818
4437
  };
3819
4438
  if (options?.domainVerification?.enabled) {
3820
4439
  const domainVerificationEndpoints = {
@@ -3923,4 +4542,4 @@ function sso(options) {
3923
4542
  };
3924
4543
  }
3925
4544
  //#endregion
3926
- 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 };