@better-auth/sso 1.4.10-beta.1 → 1.4.10

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @better-auth/sso@1.4.10-beta.1 build /home/runner/work/better-auth/better-auth/packages/sso
2
+ > @better-auth/sso@1.4.10 build /home/runner/work/better-auth/better-auth/packages/sso
3
3
  > tsdown
4
4
 
5
5
  ℹ tsdown v0.17.2 powered by rolldown v1.0.0-beta.53
@@ -7,10 +7,10 @@
7
7
  ℹ entry: src/index.ts, src/client.ts
8
8
  ℹ tsconfig: tsconfig.json
9
9
  ℹ Build start
10
- ℹ dist/index.mjs 95.91 kB │ gzip: 18.60 kB
10
+ ℹ dist/index.mjs 97.24 kB │ gzip: 18.91 kB
11
11
  ℹ dist/client.mjs  0.15 kB │ gzip: 0.14 kB
12
- ℹ dist/index.d.mts  1.48 kB │ gzip: 0.51 kB
13
- ℹ dist/client.d.mts  0.49 kB │ gzip: 0.29 kB
14
- ℹ dist/index-CvpS40sl.d.mts 43.12 kB │ gzip: 8.83 kB
15
- ℹ 5 files, total: 141.14 kB
16
- ✔ Build complete in 16051ms
12
+ ℹ dist/index.d.mts  1.67 kB │ gzip: 0.57 kB
13
+ ℹ dist/client.d.mts  0.49 kB │ gzip: 0.30 kB
14
+ ℹ dist/index-D4Ey-vkQ.d.mts 44.21 kB │ gzip: 9.10 kB
15
+ ℹ 5 files, total: 143.76 kB
16
+ ✔ Build complete in 15630ms
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as SSOPlugin } from "./index-CvpS40sl.mjs";
1
+ import { t as SSOPlugin } from "./index-D4Ey-vkQ.mjs";
2
2
 
3
3
  //#region src/client.d.ts
4
4
  interface SSOClientOptions {
@@ -367,6 +367,18 @@ interface SSOOptions {
367
367
  * ```
368
368
  */
369
369
  algorithms?: AlgorithmValidationOptions;
370
+ /**
371
+ * Maximum allowed size for SAML responses in bytes.
372
+ *
373
+ * @default 262144 (256KB)
374
+ */
375
+ maxResponseSize?: number;
376
+ /**
377
+ * Maximum allowed size for IdP metadata XML in bytes.
378
+ *
379
+ * @default 102400 (100KB)
380
+ */
381
+ maxMetadataSize?: number;
370
382
  };
371
383
  }
372
384
  //#endregion
@@ -956,6 +968,28 @@ declare const acsEndpoint: (options?: SSOOptions) => better_call0.StrictEndpoint
956
968
  };
957
969
  }, never>;
958
970
  //#endregion
971
+ //#region src/constants.d.ts
972
+ /**
973
+ * Default clock skew tolerance (5 minutes).
974
+ * Allows for minor time differences between IdP and SP servers.
975
+ *
976
+ * Accommodates:
977
+ * - Network latency and processing time
978
+ * - Clock synchronization differences (NTP drift)
979
+ * - Distributed systems across timezones
980
+ */
981
+ declare const DEFAULT_CLOCK_SKEW_MS: number;
982
+ /**
983
+ * Default maximum size for SAML responses (256 KB).
984
+ * Protects against memory exhaustion from oversized SAML payloads.
985
+ */
986
+ declare const DEFAULT_MAX_SAML_RESPONSE_SIZE: number;
987
+ /**
988
+ * Default maximum size for IdP metadata (100 KB).
989
+ * Protects against oversized metadata documents.
990
+ */
991
+ declare const DEFAULT_MAX_SAML_METADATA_SIZE: number;
992
+ //#endregion
959
993
  //#region src/oidc/types.d.ts
960
994
  /**
961
995
  * OIDC Discovery Types
@@ -1250,4 +1284,4 @@ declare function sso<O extends SSOOptions>(options?: O | undefined): {
1250
1284
  endpoints: SSOEndpoints<O>;
1251
1285
  };
1252
1286
  //#endregion
1253
- export { KeyEncryptionAlgorithm as A, SAMLConfig as C, DataEncryptionAlgorithm as D, AlgorithmValidationOptions as E, DeprecatedAlgorithmBehavior as O, OIDCConfig as S, SSOProvider as T, 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, SignatureAlgorithm as j, DigestAlgorithm as k, selectTokenEndpointAuthMethod as l, DiscoveryErrorCode as m, sso as n, needsRuntimeDiscovery as o, DiscoveryError as p, computeDiscoveryUrl as r, normalizeDiscoveryUrls as s, SSOPlugin as t, validateDiscoveryDocument as u, RequiredDiscoveryField as v, SSOOptions as w, validateSAMLTimestamp as x, SAMLConditions as y };
1287
+ export { DataEncryptionAlgorithm as A, TimestampValidationOptions as C, SSOOptions as D, SAMLConfig as E, DigestAlgorithm as M, KeyEncryptionAlgorithm as N, SSOProvider as O, SignatureAlgorithm as P, SAMLConditions as S, OIDCConfig as T, REQUIRED_DISCOVERY_FIELDS as _, fetchDiscoveryDocument as a, DEFAULT_MAX_SAML_METADATA_SIZE as b, normalizeUrl as c, validateDiscoveryUrl as d, DiscoverOIDCConfigParams as f, OIDCDiscoveryDocument as g, HydratedOIDCConfig as h, discoverOIDCConfig as i, DeprecatedAlgorithmBehavior as j, AlgorithmValidationOptions as k, selectTokenEndpointAuthMethod as l, DiscoveryErrorCode as m, sso as n, needsRuntimeDiscovery as o, DiscoveryError as p, computeDiscoveryUrl as r, normalizeDiscoveryUrls as s, SSOPlugin as t, validateDiscoveryDocument as u, RequiredDiscoveryField as v, validateSAMLTimestamp as w, DEFAULT_MAX_SAML_RESPONSE_SIZE as x, DEFAULT_CLOCK_SKEW_MS as y };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as KeyEncryptionAlgorithm, C as SAMLConfig, D as DataEncryptionAlgorithm, E as AlgorithmValidationOptions, O as DeprecatedAlgorithmBehavior, S as OIDCConfig, T as SSOProvider, _ 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 SignatureAlgorithm, k as DigestAlgorithm, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as SSOOptions, x as validateSAMLTimestamp, y as SAMLConditions } from "./index-CvpS40sl.mjs";
2
- export { AlgorithmValidationOptions, DataEncryptionAlgorithm, DeprecatedAlgorithmBehavior, DigestAlgorithm, DiscoverOIDCConfigParams, DiscoveryError, DiscoveryErrorCode, HydratedOIDCConfig, KeyEncryptionAlgorithm, OIDCConfig, OIDCDiscoveryDocument, REQUIRED_DISCOVERY_FIELDS, RequiredDiscoveryField, SAMLConditions, SAMLConfig, SSOOptions, SSOPlugin, SSOProvider, SignatureAlgorithm, TimestampValidationOptions, computeDiscoveryUrl, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
1
+ import { A as DataEncryptionAlgorithm, C as TimestampValidationOptions, D as SSOOptions, E as SAMLConfig, M as DigestAlgorithm, N as KeyEncryptionAlgorithm, O as SSOProvider, P as SignatureAlgorithm, S as SAMLConditions, T as OIDCConfig, _ as REQUIRED_DISCOVERY_FIELDS, a as fetchDiscoveryDocument, b as DEFAULT_MAX_SAML_METADATA_SIZE, c as normalizeUrl, d as validateDiscoveryUrl, f as DiscoverOIDCConfigParams, g as OIDCDiscoveryDocument, h as HydratedOIDCConfig, i as discoverOIDCConfig, j as DeprecatedAlgorithmBehavior, k as AlgorithmValidationOptions, l as selectTokenEndpointAuthMethod, m as DiscoveryErrorCode, n as sso, o as needsRuntimeDiscovery, p as DiscoveryError, r as computeDiscoveryUrl, s as normalizeDiscoveryUrls, t as SSOPlugin, u as validateDiscoveryDocument, v as RequiredDiscoveryField, w as validateSAMLTimestamp, x as DEFAULT_MAX_SAML_RESPONSE_SIZE, y as DEFAULT_CLOCK_SKEW_MS } from "./index-D4Ey-vkQ.mjs";
2
+ export { AlgorithmValidationOptions, DEFAULT_CLOCK_SKEW_MS, DEFAULT_MAX_SAML_METADATA_SIZE, DEFAULT_MAX_SAML_RESPONSE_SIZE, DataEncryptionAlgorithm, DeprecatedAlgorithmBehavior, DigestAlgorithm, DiscoverOIDCConfigParams, DiscoveryError, DiscoveryErrorCode, HydratedOIDCConfig, KeyEncryptionAlgorithm, OIDCConfig, OIDCDiscoveryDocument, REQUIRED_DISCOVERY_FIELDS, RequiredDiscoveryField, SAMLConditions, SAMLConfig, SSOOptions, SSOPlugin, SSOProvider, SignatureAlgorithm, TimestampValidationOptions, computeDiscoveryUrl, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
package/dist/index.mjs CHANGED
@@ -4,7 +4,6 @@ import * as saml from "samlify";
4
4
  import { generateRandomString } from "better-auth/crypto";
5
5
  import * as z$1 from "zod/v4";
6
6
  import z from "zod/v4";
7
- import { base64 } from "@better-auth/utils/base64";
8
7
  import { BetterFetchError, betterFetch } from "@better-fetch/fetch";
9
8
  import { HIDE_METADATA, createAuthorizationURL, generateState, parseState, validateAuthorizationCode, validateToken } from "better-auth";
10
9
  import { setSessionCookie } from "better-auth/cookies";
@@ -306,6 +305,16 @@ const DEFAULT_ASSERTION_TTL_MS = 900 * 1e3;
306
305
  * - Distributed systems across timezones
307
306
  */
308
307
  const DEFAULT_CLOCK_SKEW_MS = 300 * 1e3;
308
+ /**
309
+ * Default maximum size for SAML responses (256 KB).
310
+ * Protects against memory exhaustion from oversized SAML payloads.
311
+ */
312
+ const DEFAULT_MAX_SAML_RESPONSE_SIZE = 256 * 1024;
313
+ /**
314
+ * Default maximum size for IdP metadata (100 KB).
315
+ * Protects against oversized metadata documents.
316
+ */
317
+ const DEFAULT_MAX_SAML_METADATA_SIZE = 100 * 1024;
309
318
 
310
319
  //#endregion
311
320
  //#region src/oidc/types.ts
@@ -729,7 +738,8 @@ function normalizeDigestAlgorithm(alg) {
729
738
  const xmlParser = new XMLParser({
730
739
  ignoreAttributes: false,
731
740
  attributeNamePrefix: "@_",
732
- removeNSPrefix: true
741
+ removeNSPrefix: true,
742
+ processEntities: false
733
743
  });
734
744
  function findNode(obj, nodeName) {
735
745
  if (!obj || typeof obj !== "object") return null;
@@ -1239,6 +1249,10 @@ const registerSSOProvider = (options) => {
1239
1249
  })).length >= limit) throw new APIError("FORBIDDEN", { message: "You have reached the maximum number of SSO providers" });
1240
1250
  const body = ctx.body;
1241
1251
  if (z.string().url().safeParse(body.issuer).error) throw new APIError("BAD_REQUEST", { message: "Invalid issuer. Must be a valid URL" });
1252
+ if (body.samlConfig?.idpMetadata?.metadata) {
1253
+ const maxMetadataSize = options?.saml?.maxMetadataSize ?? DEFAULT_MAX_SAML_METADATA_SIZE;
1254
+ if (new TextEncoder().encode(body.samlConfig.idpMetadata.metadata).length > maxMetadataSize) throw new APIError("BAD_REQUEST", { message: `IdP metadata exceeds maximum allowed size (${maxMetadataSize} bytes)` });
1255
+ }
1242
1256
  if (ctx.body.organizationId) {
1243
1257
  if (!await ctx.context.adapter.findOne({
1244
1258
  model: "member",
@@ -1273,7 +1287,7 @@ const registerSSOProvider = (options) => {
1273
1287
  userInfoEndpoint: body.oidcConfig.userInfoEndpoint,
1274
1288
  tokenEndpointAuthentication: body.oidcConfig.tokenEndpointAuthentication
1275
1289
  },
1276
- isTrustedOrigin: ctx.context.isTrustedOrigin
1290
+ isTrustedOrigin: (url) => ctx.context.isTrustedOrigin(url)
1277
1291
  });
1278
1292
  } catch (error) {
1279
1293
  if (error instanceof DiscoveryError) throw mapDiscoveryErrorToAPIError(error);
@@ -1773,6 +1787,8 @@ const callbackSSOSAML = (options) => {
1773
1787
  }, async (ctx) => {
1774
1788
  const { SAMLResponse, RelayState } = ctx.body;
1775
1789
  const { providerId } = ctx.params;
1790
+ const maxResponseSize = options?.saml?.maxResponseSize ?? DEFAULT_MAX_SAML_RESPONSE_SIZE;
1791
+ if (new TextEncoder().encode(SAMLResponse).length > maxResponseSize) throw new APIError("BAD_REQUEST", { message: `SAML response exceeds maximum allowed size (${maxResponseSize} bytes)` });
1776
1792
  let provider = null;
1777
1793
  if (options?.defaultSSO?.length) {
1778
1794
  const matchingDefault = options.defaultSSO.find((defaultProvider) => defaultProvider.providerId === providerId);
@@ -1848,7 +1864,7 @@ const callbackSSOSAML = (options) => {
1848
1864
  } catch (error) {
1849
1865
  ctx.context.logger.error("SAML response validation failed", {
1850
1866
  error,
1851
- decodedResponse: new TextDecoder().decode(base64.decode(SAMLResponse))
1867
+ decodedResponse: Buffer.from(SAMLResponse, "base64").toString("utf-8")
1852
1868
  });
1853
1869
  throw new APIError("BAD_REQUEST", {
1854
1870
  message: "Invalid SAML response",
@@ -2024,6 +2040,8 @@ const acsEndpoint = (options) => {
2024
2040
  }, async (ctx) => {
2025
2041
  const { SAMLResponse, RelayState = "" } = ctx.body;
2026
2042
  const { providerId } = ctx.params;
2043
+ const maxResponseSize = options?.saml?.maxResponseSize ?? DEFAULT_MAX_SAML_RESPONSE_SIZE;
2044
+ if (new TextEncoder().encode(SAMLResponse).length > maxResponseSize) throw new APIError("BAD_REQUEST", { message: `SAML response exceeds maximum allowed size (${maxResponseSize} bytes)` });
2027
2045
  let provider = null;
2028
2046
  if (options?.defaultSSO?.length) {
2029
2047
  const matchingDefault = providerId ? options.defaultSSO.find((defaultProvider) => defaultProvider.providerId === providerId) : options.defaultSSO[0];
@@ -2082,7 +2100,7 @@ const acsEndpoint = (options) => {
2082
2100
  } catch (error) {
2083
2101
  ctx.context.logger.error("SAML response validation failed", {
2084
2102
  error,
2085
- decodedResponse: new TextDecoder().decode(base64.decode(SAMLResponse))
2103
+ decodedResponse: Buffer.from(SAMLResponse, "base64").toString("utf-8")
2086
2104
  });
2087
2105
  throw new APIError("BAD_REQUEST", {
2088
2106
  message: "Invalid SAML response",
@@ -2133,7 +2151,7 @@ const acsEndpoint = (options) => {
2133
2151
  throw ctx.redirect(`${redirectUrl}?error=unsolicited_response&error_description=IdP-initiated+SSO+not+allowed`);
2134
2152
  }
2135
2153
  }
2136
- const assertionIdAcs = extractAssertionId(new TextDecoder().decode(base64.decode(SAMLResponse)));
2154
+ const assertionIdAcs = extractAssertionId(Buffer.from(SAMLResponse, "base64").toString("utf-8"));
2137
2155
  if (assertionIdAcs) {
2138
2156
  const issuer = idp.entityMeta.getEntityID();
2139
2157
  const conditions = extract.conditions;
@@ -2332,4 +2350,4 @@ function sso(options) {
2332
2350
  }
2333
2351
 
2334
2352
  //#endregion
2335
- export { DataEncryptionAlgorithm, DigestAlgorithm, DiscoveryError, KeyEncryptionAlgorithm, REQUIRED_DISCOVERY_FIELDS, SignatureAlgorithm, computeDiscoveryUrl, discoverOIDCConfig, fetchDiscoveryDocument, needsRuntimeDiscovery, normalizeDiscoveryUrls, normalizeUrl, selectTokenEndpointAuthMethod, sso, validateDiscoveryDocument, validateDiscoveryUrl, validateSAMLTimestamp };
2353
+ 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 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@better-auth/sso",
3
3
  "author": "Bereket Engida",
4
- "version": "1.4.10-beta.1",
4
+ "version": "1.4.10",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
7
7
  "types": "dist/index.d.mts",
@@ -52,7 +52,6 @@
52
52
  }
53
53
  },
54
54
  "dependencies": {
55
- "@better-auth/utils": "0.3.0",
56
55
  "@better-fetch/fetch": "1.1.21",
57
56
  "fast-xml-parser": "^5.2.5",
58
57
  "jose": "^6.1.0",
@@ -60,6 +59,7 @@
60
59
  "zod": "^4.1.12"
61
60
  },
62
61
  "devDependencies": {
62
+ "@better-auth/utils": "0.3.0",
63
63
  "@types/body-parser": "^1.19.6",
64
64
  "@types/express": "^5.0.5",
65
65
  "better-call": "1.1.7",
@@ -67,10 +67,10 @@
67
67
  "express": "^5.1.0",
68
68
  "oauth2-mock-server": "^8.2.0",
69
69
  "tsdown": "^0.17.2",
70
- "better-auth": "1.4.10-beta.1"
70
+ "better-auth": "1.4.10"
71
71
  },
72
72
  "peerDependencies": {
73
- "better-auth": "1.4.10-beta.1"
73
+ "better-auth": "1.4.10"
74
74
  },
75
75
  "scripts": {
76
76
  "test": "vitest",
package/src/constants.ts CHANGED
@@ -40,3 +40,19 @@ export const DEFAULT_ASSERTION_TTL_MS = 15 * 60 * 1000;
40
40
  * - Distributed systems across timezones
41
41
  */
42
42
  export const DEFAULT_CLOCK_SKEW_MS = 5 * 60 * 1000;
43
+
44
+ // ============================================================================
45
+ // Size Limits (DoS Protection)
46
+ // ============================================================================
47
+
48
+ /**
49
+ * Default maximum size for SAML responses (256 KB).
50
+ * Protects against memory exhaustion from oversized SAML payloads.
51
+ */
52
+ export const DEFAULT_MAX_SAML_RESPONSE_SIZE = 256 * 1024;
53
+
54
+ /**
55
+ * Default maximum size for IdP metadata (100 KB).
56
+ * Protects against oversized metadata documents.
57
+ */
58
+ export const DEFAULT_MAX_SAML_METADATA_SIZE = 100 * 1024;
package/src/index.ts CHANGED
@@ -16,6 +16,12 @@ import {
16
16
  spMetadata,
17
17
  } from "./routes/sso";
18
18
 
19
+ export {
20
+ DEFAULT_CLOCK_SKEW_MS,
21
+ DEFAULT_MAX_SAML_METADATA_SIZE,
22
+ DEFAULT_MAX_SAML_RESPONSE_SIZE,
23
+ } from "./constants";
24
+
19
25
  export {
20
26
  type SAMLConditions,
21
27
  type TimestampValidationOptions,
package/src/routes/sso.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { base64 } from "@better-auth/utils/base64";
2
1
  import { BetterFetchError, betterFetch } from "@better-fetch/fetch";
3
2
  import type { User, Verification } from "better-auth";
4
3
  import {
@@ -37,6 +36,8 @@ import {
37
36
  DEFAULT_ASSERTION_TTL_MS,
38
37
  DEFAULT_AUTHN_REQUEST_TTL_MS,
39
38
  DEFAULT_CLOCK_SKEW_MS,
39
+ DEFAULT_MAX_SAML_METADATA_SIZE,
40
+ DEFAULT_MAX_SAML_RESPONSE_SIZE,
40
41
  USED_ASSERTION_KEY_PREFIX,
41
42
  } from "../constants";
42
43
  import { assignOrganizationFromProvider } from "../linking";
@@ -666,6 +667,20 @@ export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
666
667
  message: "Invalid issuer. Must be a valid URL",
667
668
  });
668
669
  }
670
+
671
+ if (body.samlConfig?.idpMetadata?.metadata) {
672
+ const maxMetadataSize =
673
+ options?.saml?.maxMetadataSize ?? DEFAULT_MAX_SAML_METADATA_SIZE;
674
+ if (
675
+ new TextEncoder().encode(body.samlConfig.idpMetadata.metadata)
676
+ .length > maxMetadataSize
677
+ ) {
678
+ throw new APIError("BAD_REQUEST", {
679
+ message: `IdP metadata exceeds maximum allowed size (${maxMetadataSize} bytes)`,
680
+ });
681
+ }
682
+ }
683
+
669
684
  if (ctx.body.organizationId) {
670
685
  const organization = await ctx.context.adapter.findOne({
671
686
  model: "member",
@@ -720,7 +735,7 @@ export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
720
735
  tokenEndpointAuthentication:
721
736
  body.oidcConfig.tokenEndpointAuthentication,
722
737
  },
723
- isTrustedOrigin: ctx.context.isTrustedOrigin,
738
+ isTrustedOrigin: (url: string) => ctx.context.isTrustedOrigin(url),
724
739
  });
725
740
  } catch (error) {
726
741
  if (error instanceof DiscoveryError) {
@@ -1698,6 +1713,15 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
1698
1713
  async (ctx) => {
1699
1714
  const { SAMLResponse, RelayState } = ctx.body;
1700
1715
  const { providerId } = ctx.params;
1716
+
1717
+ const maxResponseSize =
1718
+ options?.saml?.maxResponseSize ?? DEFAULT_MAX_SAML_RESPONSE_SIZE;
1719
+ if (new TextEncoder().encode(SAMLResponse).length > maxResponseSize) {
1720
+ throw new APIError("BAD_REQUEST", {
1721
+ message: `SAML response exceeds maximum allowed size (${maxResponseSize} bytes)`,
1722
+ });
1723
+ }
1724
+
1701
1725
  let provider: SSOProvider<SSOOptions> | null = null;
1702
1726
  if (options?.defaultSSO?.length) {
1703
1727
  const matchingDefault = options.defaultSSO.find(
@@ -1826,8 +1850,8 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
1826
1850
  } catch (error) {
1827
1851
  ctx.context.logger.error("SAML response validation failed", {
1828
1852
  error,
1829
- decodedResponse: new TextDecoder().decode(
1830
- base64.decode(SAMLResponse),
1853
+ decodedResponse: Buffer.from(SAMLResponse, "base64").toString(
1854
+ "utf-8",
1831
1855
  ),
1832
1856
  });
1833
1857
  throw new APIError("BAD_REQUEST", {
@@ -2137,6 +2161,14 @@ export const acsEndpoint = (options?: SSOOptions) => {
2137
2161
  const { SAMLResponse, RelayState = "" } = ctx.body;
2138
2162
  const { providerId } = ctx.params;
2139
2163
 
2164
+ const maxResponseSize =
2165
+ options?.saml?.maxResponseSize ?? DEFAULT_MAX_SAML_RESPONSE_SIZE;
2166
+ if (new TextEncoder().encode(SAMLResponse).length > maxResponseSize) {
2167
+ throw new APIError("BAD_REQUEST", {
2168
+ message: `SAML response exceeds maximum allowed size (${maxResponseSize} bytes)`,
2169
+ });
2170
+ }
2171
+
2140
2172
  // If defaultSSO is configured, use it as the provider
2141
2173
  let provider: SSOProvider<SSOOptions> | null = null;
2142
2174
 
@@ -2256,8 +2288,8 @@ export const acsEndpoint = (options?: SSOOptions) => {
2256
2288
  } catch (error) {
2257
2289
  ctx.context.logger.error("SAML response validation failed", {
2258
2290
  error,
2259
- decodedResponse: new TextDecoder().decode(
2260
- base64.decode(SAMLResponse),
2291
+ decodedResponse: Buffer.from(SAMLResponse, "base64").toString(
2292
+ "utf-8",
2261
2293
  ),
2262
2294
  });
2263
2295
  throw new APIError("BAD_REQUEST", {
@@ -2353,8 +2385,8 @@ export const acsEndpoint = (options?: SSOOptions) => {
2353
2385
  }
2354
2386
 
2355
2387
  // Assertion Replay Protection
2356
- const samlContentAcs = new TextDecoder().decode(
2357
- base64.decode(SAMLResponse),
2388
+ const samlContentAcs = Buffer.from(SAMLResponse, "base64").toString(
2389
+ "utf-8",
2358
2390
  );
2359
2391
  const assertionIdAcs = extractAssertionId(samlContentAcs);
2360
2392
 
@@ -106,6 +106,7 @@ const xmlParser = new XMLParser({
106
106
  ignoreAttributes: false,
107
107
  attributeNamePrefix: "@_",
108
108
  removeNSPrefix: true,
109
+ processEntities: false,
109
110
  });
110
111
 
111
112
  function findNode(obj: unknown, nodeName: string): unknown {
package/src/saml.test.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { createServer } from "node:http";
3
+ import { base64 } from "@better-auth/utils/base64";
3
4
  import { betterFetch } from "@better-fetch/fetch";
4
5
  import { betterAuth } from "better-auth";
5
6
  import { memoryAdapter } from "better-auth/adapters/memory";
@@ -2035,8 +2036,7 @@ describe("SAML SSO - Signature Validation Security", () => {
2035
2036
  </saml2p:Response>
2036
2037
  `;
2037
2038
 
2038
- const encodedForgedResponse =
2039
- Buffer.from(forgedSamlResponse).toString("base64");
2039
+ const encodedForgedResponse = base64.encode(forgedSamlResponse);
2040
2040
 
2041
2041
  await expect(
2042
2042
  auth.api.callbackSSOSAML({
@@ -2111,9 +2111,7 @@ describe("SAML SSO - Signature Validation Security", () => {
2111
2111
  </saml2p:Response>
2112
2112
  `;
2113
2113
 
2114
- const encodedBadSigResponse = Buffer.from(
2115
- responseWithBadSignature,
2116
- ).toString("base64");
2114
+ const encodedBadSigResponse = base64.encode(responseWithBadSignature);
2117
2115
 
2118
2116
  await expect(
2119
2117
  auth.api.callbackSSOSAML({
@@ -2360,6 +2358,16 @@ describe("SAML SSO - Timestamp Validation", () => {
2360
2358
  });
2361
2359
  });
2362
2360
 
2361
+ describe("SAML SSO - Size Limit Validation", () => {
2362
+ it("should export default size limit constants", async () => {
2363
+ const { DEFAULT_MAX_SAML_RESPONSE_SIZE, DEFAULT_MAX_SAML_METADATA_SIZE } =
2364
+ await import("./constants");
2365
+
2366
+ expect(DEFAULT_MAX_SAML_RESPONSE_SIZE).toBe(256 * 1024);
2367
+ expect(DEFAULT_MAX_SAML_METADATA_SIZE).toBe(100 * 1024);
2368
+ });
2369
+ });
2370
+
2363
2371
  describe("SAML SSO - Assertion Replay Protection", () => {
2364
2372
  it("should reject replayed SAML assertion (same assertion submitted twice)", async () => {
2365
2373
  const { auth, signInWithTestUser } = await getTestInstance({
package/src/types.ts CHANGED
@@ -341,5 +341,17 @@ export interface SSOOptions {
341
341
  * ```
342
342
  */
343
343
  algorithms?: AlgorithmValidationOptions;
344
+ /**
345
+ * Maximum allowed size for SAML responses in bytes.
346
+ *
347
+ * @default 262144 (256KB)
348
+ */
349
+ maxResponseSize?: number;
350
+ /**
351
+ * Maximum allowed size for IdP metadata XML in bytes.
352
+ *
353
+ * @default 102400 (100KB)
354
+ */
355
+ maxMetadataSize?: number;
344
356
  };
345
357
  }