@better-auth/sso 1.7.20 → 1.7.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.mts CHANGED
@@ -1,6 +1,5 @@
1
- import { t as SSOPlugin } from "./index-BvKzNnKP.mjs";
1
+ import { t as SSOPlugin } from "./index-CndkJEsk.mjs";
2
2
  import { DBFieldAttribute } from "better-auth/db";
3
- //#region src/client.d.ts
4
3
  interface SSOClientOptions {
5
4
  domainVerification?: {
6
5
  enabled: boolean;
@@ -29,5 +28,4 @@ export declare const ssoClient: <CO extends SSOClientOptions>(options?: CO | und
29
28
  "/sso/get-provider": "GET";
30
29
  };
31
30
  };
32
- //#endregion
33
31
  export {};
package/dist/client.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { t as PACKAGE_VERSION } from "./version-COmqWYke.mjs";
2
- //#region src/client.ts
1
+ import { t as PACKAGE_VERSION } from "./version-DnGxDgB4.mjs";
2
+ // src/client.ts
3
3
  const ssoClient = (options) => {
4
4
  return {
5
5
  id: "sso-client",
@@ -11,5 +11,4 @@ const ssoClient = (options) => {
11
11
  }
12
12
  };
13
13
  };
14
- //#endregion
15
14
  export { ssoClient };
@@ -6,7 +6,6 @@ import { DBFieldAttribute, FieldAttributeToObject, InferAdditionalFieldsFromPlug
6
6
  import { Awaitable, DBTransactionAdapter, OAuth2Tokens, User } from "better-auth";
7
7
  import "@better-auth/core/db/adapter";
8
8
  import "@better-auth/core";
9
- //#region src/saml/algorithms.d.ts
10
9
  declare const SignatureAlgorithm: {
11
10
  readonly RSA_SHA1: "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
12
11
  readonly RSA_SHA256: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
@@ -44,8 +43,6 @@ interface AlgorithmValidationOptions {
44
43
  allowedKeyEncryptionAlgorithms?: string[];
45
44
  allowedDataEncryptionAlgorithms?: string[];
46
45
  }
47
- //#endregion
48
- //#region src/types.d.ts
49
46
  interface OIDCMapping {
50
47
  email?: string | undefined;
51
48
  emailVerified?: string | undefined;
@@ -683,8 +680,6 @@ interface SSOOptions {
683
680
  idpInitiatedCallbackUrl?: string | undefined;
684
681
  };
685
682
  }
686
- //#endregion
687
- //#region src/routes/domain-verification.d.ts
688
683
  declare const requestDomainVerification: (options: SSOOptions) => import("better-call").StrictEndpoint<"/sso/request-domain-verification", {
689
684
  method: "POST";
690
685
  body: z.ZodObject<{
@@ -782,16 +777,12 @@ declare const verifyDomain: (options: SSOOptions) => import("better-call").Stric
782
777
  };
783
778
  }>>[];
784
779
  }, void>;
785
- //#endregion
786
- //#region src/utils.d.ts
787
780
  declare function parseCertificate(certPem: string): {
788
781
  fingerprintSha256: string;
789
782
  notBefore: string;
790
783
  notAfter: string;
791
784
  publicKeyAlgorithm: string;
792
785
  };
793
- //#endregion
794
- //#region src/routes/providers.d.ts
795
786
  type ParsedCert = ReturnType<typeof parseCertificate>;
796
787
  type SanitizedCert = ParsedCert | {
797
788
  error: string;
@@ -1150,8 +1141,6 @@ declare const deleteSSOProvider: (options?: SSOOptions) => import("better-call")
1150
1141
  }, {
1151
1142
  success: boolean;
1152
1143
  }>;
1153
- //#endregion
1154
- //#region src/routes/sso.d.ts
1155
1144
  declare const spMetadata: (options?: SSOOptions) => import("better-call").StrictEndpoint<"/sso/saml2/sp/metadata", {
1156
1145
  method: "GET";
1157
1146
  query: z.ZodObject<{
@@ -1200,7 +1189,9 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => import(
1200
1189
  }>>[];
1201
1190
  metadata: {
1202
1191
  $Infer: {
1203
- body: Record<string, any> & SSOProviderAdditionalFieldsInput<O>;
1192
+ body: {
1193
+ [x: string]: any;
1194
+ } & SSOProviderAdditionalFieldsInput<O>;
1204
1195
  };
1205
1196
  openapi: {
1206
1197
  operationId: string;
@@ -1627,8 +1618,6 @@ declare const initiateSLO: (options?: SSOOptions) => import("better-call").Stric
1627
1618
  readonly scope: "server";
1628
1619
  };
1629
1620
  }, never>;
1630
- //#endregion
1631
- //#region src/constants.d.ts
1632
1621
  /**
1633
1622
  * Default clock skew tolerance (5 minutes).
1634
1623
  * Allows for minor time differences between IdP and SP servers.
@@ -1649,8 +1638,6 @@ declare const DEFAULT_MAX_SAML_RESPONSE_SIZE: number;
1649
1638
  * Protects against oversized metadata documents.
1650
1639
  */
1651
1640
  declare const DEFAULT_MAX_SAML_METADATA_SIZE: number;
1652
- //#endregion
1653
- //#region src/routes/helpers.d.ts
1654
1641
  interface SAMLServiceProviderPolicy {
1655
1642
  /** Effective assertion-signing requirement advertised by SP metadata. */
1656
1643
  wantAssertionsSigned: boolean;
@@ -1668,8 +1655,6 @@ declare function deriveSAMLServiceProviderPolicy(config: Pick<SAMLConfig, "spMet
1668
1655
  * parsing and manual-configuration validation as SAML authentication.
1669
1656
  */
1670
1657
  declare function deriveSAMLIdentityProviderEntityID(config: SAMLConfig): string;
1671
- //#endregion
1672
- //#region src/saml/timestamp.d.ts
1673
1658
  interface TimestampValidationOptions {
1674
1659
  clockSkew?: number;
1675
1660
  requireTimestamps?: boolean;
@@ -1688,8 +1673,6 @@ interface SAMLConditions {
1688
1673
  * @throws {APIError} If timestamps are invalid, expired, or not yet valid
1689
1674
  */
1690
1675
  declare function validateSAMLTimestamp(conditions: SAMLConditions | undefined, options?: TimestampValidationOptions): void;
1691
- //#endregion
1692
- //#region src/oidc/types.d.ts
1693
1676
  /**
1694
1677
  * OIDC Discovery Types
1695
1678
  *
@@ -1856,8 +1839,6 @@ interface DiscoverOIDCConfigParams {
1856
1839
  */
1857
1840
  declare const REQUIRED_DISCOVERY_FIELDS: readonly ["issuer", "authorization_endpoint", "token_endpoint", "jwks_uri"];
1858
1841
  type RequiredDiscoveryField = (typeof REQUIRED_DISCOVERY_FIELDS)[number];
1859
- //#endregion
1860
- //#region src/oidc/discovery.d.ts
1861
1842
  /**
1862
1843
  * Main entry point: Discover and hydrate OIDC configuration from an issuer.
1863
1844
  *
@@ -1957,8 +1938,6 @@ declare function selectTokenEndpointAuthMethod(doc: OIDCDiscoveryDocument, exist
1957
1938
  * @returns true if runtime discovery should be performed
1958
1939
  */
1959
1940
  declare function needsRuntimeDiscovery(config: Partial<HydratedOIDCConfig> | undefined): boolean;
1960
- //#endregion
1961
- //#region src/index.d.ts
1962
1941
  declare module "@better-auth/core" {
1963
1942
  interface BetterAuthPluginRegistry<AuthOptions, Options> {
1964
1943
  sso: {
@@ -2022,5 +2001,4 @@ declare function sso<O extends SSOOptions>(options?: O | undefined): {
2022
2001
  };
2023
2002
  options: NoInfer<O>;
2024
2003
  };
2025
- //#endregion
2026
2004
  export { SAMLIdentityProviderMetadata as A, SSOUserResolutionContext as B, deriveSAMLIdentityProviderEntityID as C, DEFAULT_MAX_SAML_RESPONSE_SIZE as D, DEFAULT_MAX_SAML_METADATA_SIZE as E, SSOProviderMutationGuardInput as F, DigestAlgorithm as G, AlgorithmValidationOptions as H, SSOProviderReference as I, KeyEncryptionAlgorithm as K, SSOProviderUserProfile as L, SSOOptions as M, SSOProvider as N, OIDCConfig as O, SSOProviderMutationGuardContext as P, SSOSAMLUserResolutionInput as R, SAMLServiceProviderPolicy as S, DEFAULT_CLOCK_SKEW_MS as T, DataEncryptionAlgorithm as U, SSOUserResolutionInput as V, DeprecatedAlgorithmBehavior as W, REQUIRED_DISCOVERY_FIELDS as _, fetchDiscoveryDocument as a, TimestampValidationOptions as b, normalizeUrl as c, validateDiscoveryUrl as d, DiscoverOIDCConfigParams as f, OIDCDiscoveryDocument as g, HydratedOIDCConfig as h, discoverOIDCConfig as i, SSOOIDCUserResolutionInput as j, SAMLConfig as k, selectTokenEndpointAuthMethod as l, DiscoveryErrorCode as m, sso as n, needsRuntimeDiscovery as o, DiscoveryError as p, SignatureAlgorithm as q, computeDiscoveryUrl as r, normalizeDiscoveryUrls as s, SSOPlugin as t, validateDiscoveryDocument as u, RequiredDiscoveryField as v, deriveSAMLServiceProviderPolicy as w, validateSAMLTimestamp as x, SAMLConditions as y, SSOUserResolution as z };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as SAMLIdentityProviderMetadata, B as SSOUserResolutionContext, C as deriveSAMLIdentityProviderEntityID, D as DEFAULT_MAX_SAML_RESPONSE_SIZE, E as DEFAULT_MAX_SAML_METADATA_SIZE, F as SSOProviderMutationGuardInput, G as DigestAlgorithm, H as AlgorithmValidationOptions, I as SSOProviderReference, K as KeyEncryptionAlgorithm, L as SSOProviderUserProfile, M as SSOOptions, N as SSOProvider, O as OIDCConfig, P as SSOProviderMutationGuardContext, R as SSOSAMLUserResolutionInput, S as SAMLServiceProviderPolicy, T as DEFAULT_CLOCK_SKEW_MS, U as DataEncryptionAlgorithm, V as SSOUserResolutionInput, W as DeprecatedAlgorithmBehavior, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as TimestampValidationOptions, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as SSOOIDCUserResolutionInput, k as SAMLConfig, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, q as SignatureAlgorithm, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as deriveSAMLServiceProviderPolicy, x as validateSAMLTimestamp, y as SAMLConditions, z as SSOUserResolution } from "./index-BvKzNnKP.mjs";
1
+ import { A as SAMLIdentityProviderMetadata, B as SSOUserResolutionContext, C as deriveSAMLIdentityProviderEntityID, D as DEFAULT_MAX_SAML_RESPONSE_SIZE, E as DEFAULT_MAX_SAML_METADATA_SIZE, F as SSOProviderMutationGuardInput, G as DigestAlgorithm, H as AlgorithmValidationOptions, I as SSOProviderReference, K as KeyEncryptionAlgorithm, L as SSOProviderUserProfile, M as SSOOptions, N as SSOProvider, O as OIDCConfig, P as SSOProviderMutationGuardContext, R as SSOSAMLUserResolutionInput, S as SAMLServiceProviderPolicy, T as DEFAULT_CLOCK_SKEW_MS, U as DataEncryptionAlgorithm, V as SSOUserResolutionInput, W as DeprecatedAlgorithmBehavior, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as TimestampValidationOptions, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as SSOOIDCUserResolutionInput, k as SAMLConfig, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, q as SignatureAlgorithm, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as deriveSAMLServiceProviderPolicy, x as validateSAMLTimestamp, y as SAMLConditions, z as SSOUserResolution } from "./index-CndkJEsk.mjs";
2
2
  export { type AlgorithmValidationOptions, DEFAULT_CLOCK_SKEW_MS, DEFAULT_MAX_SAML_METADATA_SIZE, DEFAULT_MAX_SAML_RESPONSE_SIZE, DataEncryptionAlgorithm, type DeprecatedAlgorithmBehavior, DigestAlgorithm, type DiscoverOIDCConfigParams, DiscoveryError, type DiscoveryErrorCode, type HydratedOIDCConfig, KeyEncryptionAlgorithm, type OIDCConfig, type OIDCDiscoveryDocument, REQUIRED_DISCOVERY_FIELDS, type RequiredDiscoveryField, type SAMLConditions, type SAMLConfig, type SAMLIdentityProviderMetadata, type SAMLServiceProviderPolicy, type SSOOIDCUserResolutionInput, type SSOOptions, SSOPlugin, type SSOProvider, type SSOProviderMutationGuardContext, type SSOProviderMutationGuardInput, type SSOProviderReference, type SSOProviderUserProfile, type SSOSAMLUserResolutionInput, type SSOUserResolution, type SSOUserResolutionContext, type SSOUserResolutionInput, SignatureAlgorithm, type TimestampValidationOptions, computeDiscoveryUrl, deriveSAMLIdentityProviderEntityID, deriveSAMLServiceProviderPolicy, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { t as PACKAGE_VERSION } from "./version-COmqWYke.mjs";
2
- import { APIError, addOAuthServerContext, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
1
+ import { t as PACKAGE_VERSION } from "./version-DnGxDgB4.mjs";
2
+ import { APIError, addOAuthServerContext, createAuthEndpoint, createAuthMiddleware, generateGenericState, getSessionFromCtx, parseGenericState, 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";
@@ -17,12 +17,13 @@ import { DOMParser } from "@xmldom/xmldom";
17
17
  import samlifyDefault, * as samlifyNamespace from "samlify";
18
18
  import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
19
19
  import { parseInputData, toZodSchema } from "better-auth/db";
20
+ import { HIDE_METADATA } from "@better-auth/core/api";
21
+ import { PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, authorizationCodeRequest, createAuthorizationURL, createPrivateKeyJwtClientAssertionGetter, getOAuth2Tokens, parseStringBoolean } from "@better-auth/core/oauth2";
20
22
  import { isAPIError } from "@better-auth/core/utils/is-api-error";
21
23
  import { appendQueryParams } from "@better-auth/core/utils/url";
22
- import { HIDE_METADATA, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS, authorizationCodeRequest, createAuthorizationURL, createPrivateKeyJwtClientAssertionGetter, generateGenericState, generateState, getOAuth2Tokens, parseGenericState, parseState } from "better-auth";
23
24
  import { deleteSessionCookie, setAccountCookie, setSessionCookie } from "better-auth/cookies";
24
- import { additionalAuthorizationParamsSchema, handleOAuthUserInfo } from "better-auth/oauth2";
25
- //#region src/constants.ts
25
+ import { additionalAuthorizationParamsSchema, generateState, handleOAuthUserInfo, parseState } from "better-auth/oauth2";
26
+ // src/constants.ts
26
27
  /**
27
28
  * SAML Constants
28
29
  *
@@ -61,8 +62,7 @@ const DEFAULT_MAX_SAML_RESPONSE_SIZE = 262144;
61
62
  * Protects against oversized metadata documents.
62
63
  */
63
64
  const DEFAULT_MAX_SAML_METADATA_SIZE = 102400;
64
- //#endregion
65
- //#region src/utils.ts
65
+ // src/utils.ts
66
66
  const unsafeSAMLRedirectPathPrefix = /^\/(?:\/|\\|%2f|%5c)/i;
67
67
  const isSafeSAMLRedirectPath = (url) => url.startsWith("/") && !unsafeSAMLRedirectPathPrefix.test(url);
68
68
  /**
@@ -94,16 +94,6 @@ const domainMatches = (searchDomain, domainList) => {
94
94
  return domains.some((domain) => search === domain || search.endsWith(`.${domain}`));
95
95
  };
96
96
  /**
97
- * Strictly parse a provider-supplied email-verification claim.
98
- *
99
- * OIDC userInfo, OIDC id-token, and SAML attribute values are frequently
100
- * strings, so a loose `Boolean(value)` or truthy fallback treats the string
101
- * `"false"` as verified. Only a boolean `true` or the exact string `"true"`
102
- * count as verified; every other value, including `"false"`, `"0"`, `""`,
103
- * numbers, arrays, and objects, is unverified.
104
- */
105
- const parseProviderEmailVerified = (value) => value === true || value === "true";
106
- /**
107
97
  * Validates email domain against allowed domain(s).
108
98
  * Supports comma-separated domains for multi-domain SSO.
109
99
  */
@@ -151,8 +141,7 @@ function maskClientId(clientId) {
151
141
  if (clientId.length <= 4) return "****";
152
142
  return `****${clientId.slice(-4)}`;
153
143
  }
154
- //#endregion
155
- //#region src/linking/org-assignment.ts
144
+ // src/linking/org-assignment.ts
156
145
  const VERIFIED_PROVIDER_PAGE_SIZE = 100;
157
146
  function getEmailDomain(email) {
158
147
  const parts = email.trim().toLowerCase().split("@");
@@ -295,8 +284,7 @@ async function assignOrganizationByDomain(ctx, options) {
295
284
  ...options
296
285
  });
297
286
  }
298
- //#endregion
299
- //#region src/oidc/types.ts
287
+ // src/oidc/types.ts
300
288
  /**
301
289
  * Custom error class for OIDC discovery failures.
302
290
  * Can be caught and mapped to APIError at the edge.
@@ -321,8 +309,7 @@ const REQUIRED_DISCOVERY_FIELDS = [
321
309
  "token_endpoint",
322
310
  "jwks_uri"
323
311
  ];
324
- //#endregion
325
- //#region src/oidc/discovery.ts
312
+ // src/oidc/discovery.ts
326
313
  /**
327
314
  * OIDC Discovery Pipeline
328
315
  *
@@ -788,8 +775,7 @@ async function ensureRuntimeDiscovery(config, issuer, isTrustedOrigin) {
788
775
  await assertServerFetchedOIDCEndpointsAllowed(resolved, isTrustedOrigin);
789
776
  return resolved;
790
777
  }
791
- //#endregion
792
- //#region src/oidc/errors.ts
778
+ // src/oidc/errors.ts
793
779
  /**
794
780
  * OIDC Discovery Error Mapping
795
781
  *
@@ -869,8 +855,7 @@ function mapDiscoveryErrorToAPIError(error) {
869
855
  });
870
856
  }
871
857
  }
872
- //#endregion
873
- //#region src/provider-reference.ts
858
+ // src/provider-reference.ts
874
859
  const SSO_PROVIDER_STATE_KEY = "ssoProviderReference";
875
860
  function serializeCanonical(value) {
876
861
  if (value === null) return "null";
@@ -965,8 +950,7 @@ async function isCurrentSSOProviderReference(provider, reference) {
965
950
  if (source.type === "persisted" && (reference.source.type !== "persisted" || source.recordId !== reference.source.recordId)) return false;
966
951
  return reference.authenticationConfigurationFingerprint === await computeProviderAuthenticationFingerprint(provider);
967
952
  }
968
- //#endregion
969
- //#region src/saml/parser.ts
953
+ // src/saml/parser.ts
970
954
  const xmlParser = new XMLParser({
971
955
  ignoreAttributes: false,
972
956
  attributeNamePrefix: "@_",
@@ -999,8 +983,7 @@ function countAllNodes(obj, nodeName) {
999
983
  else if (typeof value === "object" && value !== null) count += countAllNodes(value, nodeName);
1000
984
  return count;
1001
985
  }
1002
- //#endregion
1003
- //#region src/saml/algorithms.ts
986
+ // src/saml/algorithms.ts
1004
987
  const SignatureAlgorithm = {
1005
988
  RSA_SHA1: "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
1006
989
  RSA_SHA256: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
@@ -1177,8 +1160,7 @@ function validateConfigAlgorithms(config, options = {}) {
1177
1160
  }
1178
1161
  }
1179
1162
  const saml = typeof samlifyNamespace.SPMetadata === "function" && typeof samlifyNamespace.setSchemaValidator === "function" ? samlifyNamespace : samlifyDefault ?? samlifyNamespace;
1180
- //#endregion
1181
- //#region src/saml/assertions.ts
1163
+ // src/saml/assertions.ts
1182
1164
  const SAML_ASSERTION_NAMESPACE = "urn:oasis:names:tc:SAML:2.0:assertion";
1183
1165
  const SAML_PROTOCOL_NAMESPACE = "urn:oasis:names:tc:SAML:2.0:protocol";
1184
1166
  const XML_SIGNATURE_NAMESPACE = "http://www.w3.org/2000/09/xmldsig#";
@@ -1254,8 +1236,7 @@ function verifySAMLAssertionSignature(samlContent, verificationOptions) {
1254
1236
  throw assertionSignatureFailure();
1255
1237
  }
1256
1238
  }
1257
- //#endregion
1258
- //#region src/saml/error-codes.ts
1239
+ // src/saml/error-codes.ts
1259
1240
  const SAML_ERROR_CODES = defineErrorCodes({
1260
1241
  SINGLE_LOGOUT_NOT_ENABLED: "Single Logout is not enabled",
1261
1242
  INVALID_LOGOUT_RESPONSE: "Invalid LogoutResponse",
@@ -1265,8 +1246,7 @@ const SAML_ERROR_CODES = defineErrorCodes({
1265
1246
  SAML_PROVIDER_NOT_FOUND: "SAML provider not found",
1266
1247
  CERT_SOURCE_MISSING: "samlConfig requires either a signing certificate (cert or idpMetadata.cert) or an idpMetadata.metadata XML document."
1267
1248
  });
1268
- //#endregion
1269
- //#region src/saml/cert.ts
1249
+ // src/saml/cert.ts
1270
1250
  /**
1271
1251
  * IdP signing-certificate rules for SAML configs. Centralized so the runtime
1272
1252
  * verification path (`createIdP`), the sanitizer (`getSSOProvider` and
@@ -1295,8 +1275,7 @@ function validateCertSources(config) {
1295
1275
  const hasExplicitCert = config.idpMetadata?.cert !== void 0 || config.cert !== void 0;
1296
1276
  if (!hasMetadataXml && !hasExplicitCert) throw APIError.from("BAD_REQUEST", SAML_ERROR_CODES.CERT_SOURCE_MISSING);
1297
1277
  }
1298
- //#endregion
1299
- //#region src/saml/response-binding.ts
1278
+ // src/saml/response-binding.ts
1300
1279
  const SAML_HTTP_POST_BINDING = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
1301
1280
  const SAML_BEARER_CONFIRMATION_METHOD = "urn:oasis:names:tc:SAML:2.0:cm:bearer";
1302
1281
  const SAML_METADATA_NAMESPACE = "urn:oasis:names:tc:SAML:2.0:metadata";
@@ -1473,8 +1452,7 @@ function validateSAMLResponseBinding(samlContent, options) {
1473
1452
  validateBearerRecipient(assertion, expectedRecipients);
1474
1453
  validateResponseDestination(response, expectedRecipients);
1475
1454
  }
1476
- //#endregion
1477
- //#region src/saml/response-validation.ts
1455
+ // src/saml/response-validation.ts
1478
1456
  function errorRedirectUrl(base, error, description) {
1479
1457
  try {
1480
1458
  const url = new URL(base);
@@ -1571,8 +1549,7 @@ function validateAudience(c, ctx) {
1571
1549
  throw c.redirect(errorRedirectUrl(ctx.redirectUrl, "invalid_saml_response", "Audience mismatch"));
1572
1550
  }
1573
1551
  }
1574
- //#endregion
1575
- //#region src/user-resolution.ts
1552
+ // src/user-resolution.ts
1576
1553
  const SSO_AUTHENTICATION_FAILURE = Symbol("SSO_AUTHENTICATION_FAILURE");
1577
1554
  function isRecord$1(value) {
1578
1555
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -1658,8 +1635,7 @@ function getFailedSSOAuthenticationResult(error) {
1658
1635
  if (!(error instanceof Error) || !(SSO_AUTHENTICATION_FAILURE in error) || error[SSO_AUTHENTICATION_FAILURE] !== true || !("result" in error)) return;
1659
1636
  return error.result;
1660
1637
  }
1661
- //#endregion
1662
- //#region src/routes/helpers.ts
1638
+ // src/routes/helpers.ts
1663
1639
  /**
1664
1640
  * Same as `normalizePem`, but applied across the resolved list of IdP signing
1665
1641
  * certificates so multi-cert rotation configs survive the line-trim step.
@@ -1823,8 +1799,7 @@ function createSAMLPostForm(action, samlParam, samlValue, relayState) {
1823
1799
  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>`;
1824
1800
  return new Response(html, { headers: { "Content-Type": "text/html" } });
1825
1801
  }
1826
- //#endregion
1827
- //#region src/routes/schemas.ts
1802
+ // src/routes/schemas.ts
1828
1803
  const absoluteUrlSchema = z.url();
1829
1804
  const samlRedirectUrlSchema = z.string().refine((url) => isSafeSAMLRedirectPath(url) || absoluteUrlSchema.safeParse(url).success, { message: "Expected an absolute URL or a relative path starting with /" });
1830
1805
  function getSSOProviderAdditionalFields$1(options) {
@@ -1967,8 +1942,7 @@ function getUpdateSSOProviderBodySchema(options) {
1967
1942
  ...getSSOProviderAdditionalFieldsSchema(options).partial().shape
1968
1943
  });
1969
1944
  }
1970
- //#endregion
1971
- //#region src/routes/providers.ts
1945
+ // src/routes/providers.ts
1972
1946
  const ADMIN_ROLES = ["owner", "admin"];
1973
1947
  const OIDC_IDENTITY_BOUNDARY_FIELDS = [
1974
1948
  "authorizationEndpoint",
@@ -2519,8 +2493,7 @@ const deleteSSOProvider = (options) => {
2519
2493
  return ctx.json({ success: true });
2520
2494
  });
2521
2495
  };
2522
- //#endregion
2523
- //#region src/routes/domain-verification.ts
2496
+ // src/routes/domain-verification.ts
2524
2497
  const DNS_LABEL_MAX_LENGTH = 63;
2525
2498
  const DEFAULT_TOKEN_PREFIX = "better-auth-token";
2526
2499
  const domainVerificationBodySchema = z.object({ providerId: z.string() });
@@ -2649,8 +2622,7 @@ const verifyDomain = (options) => {
2649
2622
  ctx.setStatus(204);
2650
2623
  });
2651
2624
  };
2652
- //#endregion
2653
- //#region src/saml-state.ts
2625
+ // src/saml-state.ts
2654
2626
  async function generateRelayState(c, link, providerReference) {
2655
2627
  const callbackURL = c.body.callbackURL;
2656
2628
  if (!callbackURL) throw new APIError("BAD_REQUEST", { message: "callbackURL is required" });
@@ -2700,8 +2672,7 @@ async function parseRelayState(c) {
2700
2672
  if (!parsedData.errorURL) parsedData.errorURL = errorURL;
2701
2673
  return parsedData;
2702
2674
  }
2703
- //#endregion
2704
- //#region src/saml/timestamp.ts
2675
+ // src/saml/timestamp.ts
2705
2676
  /**
2706
2677
  * Validates SAML assertion timestamp conditions (NotBefore/NotOnOrAfter).
2707
2678
  * Prevents acceptance of expired or future-dated assertions.
@@ -2741,8 +2712,7 @@ function validateSAMLTimestamp(conditions, options = {}) {
2741
2712
  });
2742
2713
  }
2743
2714
  }
2744
- //#endregion
2745
- //#region src/routes/saml-pipeline.ts
2715
+ // src/routes/saml-pipeline.ts
2746
2716
  function isSameSSOProviderReference(left, right) {
2747
2717
  if (left.providerId !== right.providerId || left.authenticationConfigurationFingerprint !== right.authenticationConfigurationFingerprint || left.source.type !== right.source.type) return false;
2748
2718
  return left.source.type === "configured" ? true : right.source.type === "persisted" && left.source.recordId === right.source.recordId;
@@ -2823,20 +2793,16 @@ async function getSAMLResponseBindingContent(sp, samlContent) {
2823
2793
  /**
2824
2794
  * Extracts the Assertion ID from a SAML response XML.
2825
2795
  * Used for replay protection per SAML 2.0 Core section 2.3.3.
2796
+ *
2797
+ * Reads the ID through the DOM so character references are decoded the same
2798
+ * way the signature check decodes them: re-spelling the ID keeps the
2799
+ * signature valid, so it must not change the replay key.
2826
2800
  */
2827
2801
  function extractAssertionId(samlContent) {
2828
2802
  try {
2829
- const parsed = new XMLParser({
2830
- ignoreAttributes: false,
2831
- attributeNamePrefix: "@_",
2832
- removeNSPrefix: true
2833
- }).parse(samlContent);
2834
- const response = parsed.Response || parsed["samlp:Response"];
2835
- if (!response) return null;
2836
- const rawAssertion = response.Assertion || response["saml:Assertion"];
2837
- const assertion = Array.isArray(rawAssertion) ? rawAssertion[0] : rawAssertion;
2838
- if (!assertion) return null;
2839
- return assertion["@_ID"] || null;
2803
+ const response = new DOMParser().parseFromString(samlContent, "text/xml").documentElement;
2804
+ if (response?.localName !== "Response") return null;
2805
+ return Array.from(response.childNodes).find((node) => node.nodeType === 1 && node.localName === "Assertion")?.getAttribute("ID") || null;
2840
2806
  } catch {
2841
2807
  return null;
2842
2808
  }
@@ -2999,7 +2965,7 @@ async function processSAMLResponse(ctx, params, options) {
2999
2965
  id: extract.nameID,
3000
2966
  email: (attr(mapping.email || "email") || extract.nameID || "").toLowerCase(),
3001
2967
  name: [attr(mapping.firstName || "givenName"), attr(mapping.lastName || "surname")].filter(Boolean).join(" ") || attr(mapping.name || "displayName") || extract.nameID,
3002
- emailVerified: options?.trustEmailVerified && mapping.emailVerified ? parseProviderEmailVerified(attr(mapping.emailVerified)) : false
2968
+ emailVerified: options?.trustEmailVerified && mapping.emailVerified ? parseStringBoolean(attr(mapping.emailVerified)) ?? false : false
3003
2969
  };
3004
2970
  if (!userInfo.id || !userInfo.email) {
3005
2971
  ctx.context.logger.error("Missing essential user info from SAML response", {
@@ -3140,8 +3106,7 @@ async function processSAMLResponse(ctx, params, options) {
3140
3106
  }
3141
3107
  return samlRedirectUrl;
3142
3108
  }
3143
- //#endregion
3144
- //#region src/routes/sso.ts
3109
+ // src/routes/sso.ts
3145
3110
  const BUILT_IN_ACCOUNT_PROVIDER_IDS = [
3146
3111
  "credential",
3147
3112
  "email-otp",
@@ -3939,7 +3904,7 @@ async function handleOIDCCallback(ctx, options, providerId, stateData, parsedPro
3939
3904
  ...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, rawUserInfo[value]])),
3940
3905
  id: readStringClaim(rawUserInfo, "sub"),
3941
3906
  email: readEmailClaim(rawUserInfo),
3942
- emailVerified: options?.trustEmailVerified ? parseProviderEmailVerified(rawUserInfo[mapping.emailVerified || "email_verified"]) : false,
3907
+ emailVerified: options?.trustEmailVerified ? parseStringBoolean(rawUserInfo[mapping.emailVerified || "email_verified"]) ?? false : false,
3943
3908
  name: readStringClaim(rawUserInfo, mapping.name || "name"),
3944
3909
  image: readStringClaim(rawUserInfo, mapping.image || "picture")
3945
3910
  };
@@ -3950,7 +3915,7 @@ async function handleOIDCCallback(ctx, options, providerId, stateData, parsedPro
3950
3915
  ...Object.fromEntries(Object.entries(mapping.extraFields || {}).map(([key, value]) => [key, idToken[value]])),
3951
3916
  id: idToken.sub,
3952
3917
  email: readEmailClaim(idToken),
3953
- emailVerified: options?.trustEmailVerified ? parseProviderEmailVerified(idToken[mapping.emailVerified || "email_verified"]) : false,
3918
+ emailVerified: options?.trustEmailVerified ? parseStringBoolean(idToken[mapping.emailVerified || "email_verified"]) ?? false : false,
3954
3919
  name: readStringClaim(idToken, mapping.name || "name"),
3955
3920
  image: readStringClaim(idToken, mapping.image || "picture")
3956
3921
  };
@@ -4423,7 +4388,7 @@ const initiateSLO = (options) => {
4423
4388
  const session = ctx.context.session;
4424
4389
  const sessionLookupKey = `${SAML_SESSION_BY_ID_PREFIX}${session.session.id}`;
4425
4390
  const sessionLookup = await ctx.context.internalAdapter.findVerificationValue(sessionLookupKey);
4426
- let nameID = session.user.email;
4391
+ let nameID;
4427
4392
  let sessionIndex;
4428
4393
  let samlSessionKey;
4429
4394
  if (sessionLookup) {
@@ -4432,11 +4397,15 @@ const initiateSLO = (options) => {
4432
4397
  if (stored) {
4433
4398
  const data = safeJsonParse(stored.value);
4434
4399
  if (data) {
4435
- nameID = data.nameID || nameID;
4400
+ nameID = data.nameID;
4436
4401
  sessionIndex = data.sessionIndex;
4437
4402
  }
4438
4403
  }
4439
4404
  }
4405
+ if (!nameID) {
4406
+ const [account] = (await ctx.context.internalAdapter.findAccounts(session.user.id)).filter((account) => account.providerId === providerId).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
4407
+ nameID = account?.accountId ?? session.user.email;
4408
+ }
4440
4409
  const logoutRequest = sp.createLogoutRequest(idp, "redirect", {
4441
4410
  logoutNameID: nameID,
4442
4411
  sessionIndex,
@@ -4455,8 +4424,7 @@ const initiateSLO = (options) => {
4455
4424
  throw ctx.redirect(logoutRequest.context);
4456
4425
  });
4457
4426
  };
4458
- //#endregion
4459
- //#region src/index.ts
4427
+ // src/index.ts
4460
4428
  saml.setSchemaValidator({ async validate(xml) {
4461
4429
  if (XMLValidator.validate(xml, { allowBooleanAttributes: true }) === true) return "SUCCESS_VALIDATE_XML";
4462
4430
  throw "ERR_INVALID_XML";
@@ -4501,6 +4469,53 @@ function assertNoAdditionalFieldCollisions(options) {
4501
4469
  if (builtInFieldNames.has(fieldName)) throw new Error(`ssoProvider additional field "${key}" maps to built-in field "${fieldName}"`);
4502
4470
  }
4503
4471
  }
4472
+ const SAML_SESSION_RECORD_PAGE_SIZE = 100;
4473
+ async function findSAMLSessionRecord(ctx, identifier, isOwnRecord) {
4474
+ for (let offset = 0;; offset += SAML_SESSION_RECORD_PAGE_SIZE) {
4475
+ const page = await ctx.adapter.findMany({
4476
+ model: "verification",
4477
+ where: [{
4478
+ field: "identifier",
4479
+ value: identifier
4480
+ }],
4481
+ sortBy: {
4482
+ field: "createdAt",
4483
+ direction: "desc"
4484
+ },
4485
+ limit: SAML_SESSION_RECORD_PAGE_SIZE,
4486
+ offset
4487
+ });
4488
+ const record = page.find(isOwnRecord);
4489
+ if (record || page.length < SAML_SESSION_RECORD_PAGE_SIZE) return record;
4490
+ }
4491
+ }
4492
+ /**
4493
+ * SLO records are written with the session's expiry at sign-in. Move it when a
4494
+ * refresh extends the session, so expired-row cleanup doesn't drop them early.
4495
+ */
4496
+ async function extendSAMLSessionRecords(ctx, session) {
4497
+ const lookupKey = `${SAML_SESSION_BY_ID_PREFIX}${session.id}`;
4498
+ const lookup = await ctx.internalAdapter.findVerificationValue(lookupKey);
4499
+ const expiresAt = new Date(session.expiresAt);
4500
+ if (!lookup || new Date(lookup.expiresAt) >= expiresAt) return;
4501
+ const isOwnRecord = (record) => safeJsonParse(record.value)?.sessionId === session.id;
4502
+ const latest = await ctx.internalAdapter.findVerificationValue(lookup.value);
4503
+ const secondaryStorageOnly = ctx.options.secondaryStorage && !ctx.options.verification?.storeInDatabase;
4504
+ if (latest && secondaryStorageOnly) {
4505
+ if (isOwnRecord(latest)) await ctx.internalAdapter.updateVerificationByIdentifier(lookup.value, { expiresAt });
4506
+ } else if (latest) {
4507
+ const record = await findSAMLSessionRecord(ctx, latest.identifier, isOwnRecord);
4508
+ if (record) await ctx.adapter.update({
4509
+ model: "verification",
4510
+ where: [{
4511
+ field: "id",
4512
+ value: record.id
4513
+ }],
4514
+ update: { expiresAt }
4515
+ });
4516
+ }
4517
+ await ctx.internalAdapter.updateVerificationByIdentifier(lookupKey, { expiresAt });
4518
+ }
4504
4519
  function sso(options) {
4505
4520
  assertNoAdditionalFieldCollisions(options);
4506
4521
  const optionsWithStore = options;
@@ -4533,8 +4548,13 @@ function sso(options) {
4533
4548
  version: PACKAGE_VERSION,
4534
4549
  init(ctx) {
4535
4550
  const existing = ctx.skipOriginCheck;
4536
- if (existing === true) return {};
4537
- return { context: { skipOriginCheck: [...Array.isArray(existing) ? existing : [], ...SAML_SKIP_ORIGIN_CHECK_PATHS] } };
4551
+ return {
4552
+ ...existing !== true && { context: { skipOriginCheck: [...Array.isArray(existing) ? existing : [], ...SAML_SKIP_ORIGIN_CHECK_PATHS] } },
4553
+ options: { databaseHooks: { session: { update: { async after(session) {
4554
+ if (!options?.saml?.enableSingleLogout) return;
4555
+ await extendSAMLSessionRecords(ctx, session).catch((error) => ctx.logger.warn("Failed to extend SAML session records", { error }));
4556
+ } } } } }
4557
+ };
4538
4558
  },
4539
4559
  endpoints,
4540
4560
  hooks: {
@@ -4624,5 +4644,4 @@ function sso(options) {
4624
4644
  options
4625
4645
  };
4626
4646
  }
4627
- //#endregion
4628
4647
  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 };
@@ -0,0 +1,3 @@
1
+ // src/version.ts
2
+ const PACKAGE_VERSION = "1.7.22";
3
+ export { PACKAGE_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/sso",
3
- "version": "1.7.20",
3
+ "version": "1.7.22",
4
4
  "description": "SSO plugin for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -64,24 +64,24 @@
64
64
  "zod": "^4.5.4"
65
65
  },
66
66
  "devDependencies": {
67
+ "@better-auth/core": "1.7.22",
68
+ "@better-auth/kysely-adapter": "1.7.22",
67
69
  "@types/body-parser": "^1.19.6",
68
70
  "@types/express": "^5.0.6",
71
+ "better-auth": "1.7.22",
69
72
  "better-call": "1.4.5",
70
73
  "body-parser": "^2.3.0",
71
74
  "express": "^5.2.1",
72
75
  "kysely": "^0.28.17 || ^0.29.0",
73
76
  "oauth2-mock-server": "^9.0.0",
74
- "tsdown": "0.23.1",
75
- "@better-auth/core": "1.7.20",
76
- "@better-auth/kysely-adapter": "1.7.20",
77
- "better-auth": "1.7.20"
77
+ "tsdown": "0.23.1"
78
78
  },
79
79
  "peerDependencies": {
80
+ "@better-auth/core": "^1.7.22",
80
81
  "@better-auth/utils": "^0.4.2 || ^0.5.0",
81
82
  "@better-fetch/fetch": "1.3.3",
82
- "better-call": "1.4.5",
83
- "@better-auth/core": "^1.7.20",
84
- "better-auth": "^1.7.20"
83
+ "better-auth": "^1.7.22",
84
+ "better-call": "1.4.5"
85
85
  },
86
86
  "scripts": {
87
87
  "build": "tsdown",
@@ -1,5 +0,0 @@
1
- //#endregion
2
- //#region src/version.ts
3
- const PACKAGE_VERSION = "1.7.20";
4
- //#endregion
5
- export { PACKAGE_VERSION as t };