@better-auth/sso 1.4.10-beta.1 → 1.5.0-beta.2
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/.turbo/turbo-build.log +7 -7
- package/dist/client.d.mts +1 -1
- package/dist/{index-CvpS40sl.d.mts → index-D4Ey-vkQ.d.mts} +35 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +25 -7
- package/package.json +4 -4
- package/src/constants.ts +16 -0
- package/src/index.ts +6 -0
- package/src/routes/sso.ts +40 -8
- package/src/saml/algorithms.ts +1 -0
- package/src/saml.test.ts +13 -5
- package/src/types.ts +12 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @better-auth/sso@1.
|
|
2
|
+
> @better-auth/sso@1.5.0-beta.2 build /home/runner/work/better-auth/better-auth/packages/sso
|
|
3
3
|
> tsdown
|
|
4
4
|
|
|
5
5
|
[34mℹ[39m tsdown [2mv0.17.2[22m powered by rolldown [2mv1.0.0-beta.53[22m
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
[34mℹ[39m entry: [34msrc/index.ts, src/client.ts[39m
|
|
8
8
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
9
|
[34mℹ[39m Build start
|
|
10
|
-
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [
|
|
10
|
+
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m97.24 kB[22m [2m│ gzip: 18.91 kB[22m
|
|
11
11
|
[34mℹ[39m [2mdist/[22m[1mclient.mjs[22m [2m 0.15 kB[22m [2m│ gzip: 0.14 kB[22m
|
|
12
|
-
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 1.
|
|
13
|
-
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 0.49 kB[22m [2m│ gzip: 0.
|
|
14
|
-
[34mℹ[39m [2mdist/[22m[32mindex-
|
|
15
|
-
[34mℹ[39m 5 files, total:
|
|
16
|
-
[32m✔[39m Build complete in [
|
|
12
|
+
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 1.67 kB[22m [2m│ gzip: 0.57 kB[22m
|
|
13
|
+
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 0.49 kB[22m [2m│ gzip: 0.30 kB[22m
|
|
14
|
+
[34mℹ[39m [2mdist/[22m[32mindex-D4Ey-vkQ.d.mts[39m [2m44.21 kB[22m [2m│ gzip: 9.10 kB[22m
|
|
15
|
+
[34mℹ[39m 5 files, total: 143.76 kB
|
|
16
|
+
[32m✔[39m Build complete in [32m15849ms[39m
|
package/dist/client.d.mts
CHANGED
|
@@ -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 {
|
|
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
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
+
"version": "1.5.0-beta.2",
|
|
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.
|
|
70
|
+
"better-auth": "1.5.0-beta.2"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"better-auth": "1.
|
|
73
|
+
"better-auth": "1.5.0-beta.2"
|
|
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:
|
|
1830
|
-
|
|
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:
|
|
2260
|
-
|
|
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 =
|
|
2357
|
-
|
|
2388
|
+
const samlContentAcs = Buffer.from(SAMLResponse, "base64").toString(
|
|
2389
|
+
"utf-8",
|
|
2358
2390
|
);
|
|
2359
2391
|
const assertionIdAcs = extractAssertionId(samlContentAcs);
|
|
2360
2392
|
|
package/src/saml/algorithms.ts
CHANGED
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 =
|
|
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
|
}
|