@better-auth/sso 1.4.8-beta.7 → 1.4.9
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 +6 -6
- package/dist/client.d.mts +1 -1
- package/dist/{index-DNWhGQW-.d.mts → index-CvpS40sl.d.mts} +9 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +71 -5
- package/package.json +4 -3
- package/src/routes/sso.ts +31 -12
- package/src/saml/algorithms.test.ts +244 -0
- package/src/saml/algorithms.ts +109 -0
- package/src/saml/index.ts +2 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @better-auth/sso@1.4.
|
|
2
|
+
> @better-auth/sso@1.4.9 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 [2m95.91 kB[22m [2m│ gzip: 18.60 kB[22m
|
|
11
11
|
[34mℹ[39m [2mdist/[22m[1mclient.mjs[22m [2m 0.15 kB[22m [2m│ gzip: 0.14 kB[22m
|
|
12
12
|
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 1.48 kB[22m [2m│ gzip: 0.51 kB[22m
|
|
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 [
|
|
13
|
+
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 0.49 kB[22m [2m│ gzip: 0.29 kB[22m
|
|
14
|
+
[34mℹ[39m [2mdist/[22m[32mindex-CvpS40sl.d.mts[39m [2m43.12 kB[22m [2m│ gzip: 8.83 kB[22m
|
|
15
|
+
[34mℹ[39m 5 files, total: 141.14 kB
|
|
16
|
+
[32m✔[39m Build complete in [32m22963ms[39m
|
package/dist/client.d.mts
CHANGED
|
@@ -776,9 +776,17 @@ declare const registerSSOProvider: <O extends SSOOptions>(options: O) => better_
|
|
|
776
776
|
}, O["domainVerification"] extends {
|
|
777
777
|
enabled: true;
|
|
778
778
|
} ? {
|
|
779
|
+
redirectURI: string;
|
|
780
|
+
oidcConfig: OIDCConfig | null;
|
|
781
|
+
samlConfig: SAMLConfig | null;
|
|
782
|
+
} & Omit<SSOProvider<O>, "oidcConfig" | "samlConfig"> & {
|
|
779
783
|
domainVerified: boolean;
|
|
780
784
|
domainVerificationToken: string;
|
|
781
|
-
}
|
|
785
|
+
} : {
|
|
786
|
+
redirectURI: string;
|
|
787
|
+
oidcConfig: OIDCConfig | null;
|
|
788
|
+
samlConfig: SAMLConfig | null;
|
|
789
|
+
} & Omit<SSOProvider<O>, "oidcConfig" | "samlConfig">>;
|
|
782
790
|
declare const signInSSO: (options?: SSOOptions) => better_call0.StrictEndpoint<"/sign-in/sso", {
|
|
783
791
|
method: "POST";
|
|
784
792
|
body: z.ZodObject<{
|
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-
|
|
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
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 };
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ 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";
|
|
7
8
|
import { BetterFetchError, betterFetch } from "@better-fetch/fetch";
|
|
8
9
|
import { HIDE_METADATA, createAuthorizationURL, generateState, parseState, validateAuthorizationCode, validateToken } from "better-auth";
|
|
9
10
|
import { setSessionCookie } from "better-auth/cookies";
|
|
@@ -686,6 +687,7 @@ const DataEncryptionAlgorithm = {
|
|
|
686
687
|
const DEPRECATED_SIGNATURE_ALGORITHMS = [SignatureAlgorithm.RSA_SHA1];
|
|
687
688
|
const DEPRECATED_KEY_ENCRYPTION_ALGORITHMS = [KeyEncryptionAlgorithm.RSA_1_5];
|
|
688
689
|
const DEPRECATED_DATA_ENCRYPTION_ALGORITHMS = [DataEncryptionAlgorithm.TRIPLEDES_CBC];
|
|
690
|
+
const DEPRECATED_DIGEST_ALGORITHMS = [DigestAlgorithm.SHA1];
|
|
689
691
|
const SECURE_SIGNATURE_ALGORITHMS = [
|
|
690
692
|
SignatureAlgorithm.RSA_SHA256,
|
|
691
693
|
SignatureAlgorithm.RSA_SHA384,
|
|
@@ -694,6 +696,36 @@ const SECURE_SIGNATURE_ALGORITHMS = [
|
|
|
694
696
|
SignatureAlgorithm.ECDSA_SHA384,
|
|
695
697
|
SignatureAlgorithm.ECDSA_SHA512
|
|
696
698
|
];
|
|
699
|
+
const SECURE_DIGEST_ALGORITHMS = [
|
|
700
|
+
DigestAlgorithm.SHA256,
|
|
701
|
+
DigestAlgorithm.SHA384,
|
|
702
|
+
DigestAlgorithm.SHA512
|
|
703
|
+
];
|
|
704
|
+
const SHORT_FORM_SIGNATURE_TO_URI = {
|
|
705
|
+
sha1: SignatureAlgorithm.RSA_SHA1,
|
|
706
|
+
sha256: SignatureAlgorithm.RSA_SHA256,
|
|
707
|
+
sha384: SignatureAlgorithm.RSA_SHA384,
|
|
708
|
+
sha512: SignatureAlgorithm.RSA_SHA512,
|
|
709
|
+
"rsa-sha1": SignatureAlgorithm.RSA_SHA1,
|
|
710
|
+
"rsa-sha256": SignatureAlgorithm.RSA_SHA256,
|
|
711
|
+
"rsa-sha384": SignatureAlgorithm.RSA_SHA384,
|
|
712
|
+
"rsa-sha512": SignatureAlgorithm.RSA_SHA512,
|
|
713
|
+
"ecdsa-sha256": SignatureAlgorithm.ECDSA_SHA256,
|
|
714
|
+
"ecdsa-sha384": SignatureAlgorithm.ECDSA_SHA384,
|
|
715
|
+
"ecdsa-sha512": SignatureAlgorithm.ECDSA_SHA512
|
|
716
|
+
};
|
|
717
|
+
const SHORT_FORM_DIGEST_TO_URI = {
|
|
718
|
+
sha1: DigestAlgorithm.SHA1,
|
|
719
|
+
sha256: DigestAlgorithm.SHA256,
|
|
720
|
+
sha384: DigestAlgorithm.SHA384,
|
|
721
|
+
sha512: DigestAlgorithm.SHA512
|
|
722
|
+
};
|
|
723
|
+
function normalizeSignatureAlgorithm(alg) {
|
|
724
|
+
return SHORT_FORM_SIGNATURE_TO_URI[alg.toLowerCase()] ?? alg;
|
|
725
|
+
}
|
|
726
|
+
function normalizeDigestAlgorithm(alg) {
|
|
727
|
+
return SHORT_FORM_DIGEST_TO_URI[alg.toLowerCase()] ?? alg;
|
|
728
|
+
}
|
|
697
729
|
const xmlParser = new XMLParser({
|
|
698
730
|
ignoreAttributes: false,
|
|
699
731
|
attributeNamePrefix: "@_",
|
|
@@ -791,6 +823,35 @@ function validateSAMLAlgorithms(response, options) {
|
|
|
791
823
|
validateSignatureAlgorithm(response.sigAlg, options);
|
|
792
824
|
if (hasEncryptedAssertion(response.samlContent)) validateEncryptionAlgorithms(extractEncryptionAlgorithms(response.samlContent), options);
|
|
793
825
|
}
|
|
826
|
+
function validateConfigAlgorithms(config, options = {}) {
|
|
827
|
+
const { onDeprecated = "warn", allowedSignatureAlgorithms, allowedDigestAlgorithms } = options;
|
|
828
|
+
if (config.signatureAlgorithm) {
|
|
829
|
+
const normalized = normalizeSignatureAlgorithm(config.signatureAlgorithm);
|
|
830
|
+
if (allowedSignatureAlgorithms) {
|
|
831
|
+
if (!allowedSignatureAlgorithms.map(normalizeSignatureAlgorithm).includes(normalized)) throw new APIError("BAD_REQUEST", {
|
|
832
|
+
message: `SAML signature algorithm not in allow-list: ${config.signatureAlgorithm}`,
|
|
833
|
+
code: "SAML_ALGORITHM_NOT_ALLOWED"
|
|
834
|
+
});
|
|
835
|
+
} else if (DEPRECATED_SIGNATURE_ALGORITHMS.includes(normalized)) handleDeprecatedAlgorithm(`SAML config uses deprecated signature algorithm: ${config.signatureAlgorithm}. Consider using SHA-256 or stronger.`, onDeprecated, "SAML_DEPRECATED_CONFIG_ALGORITHM");
|
|
836
|
+
else if (!SECURE_SIGNATURE_ALGORITHMS.includes(normalized)) throw new APIError("BAD_REQUEST", {
|
|
837
|
+
message: `SAML signature algorithm not recognized: ${config.signatureAlgorithm}`,
|
|
838
|
+
code: "SAML_UNKNOWN_ALGORITHM"
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
if (config.digestAlgorithm) {
|
|
842
|
+
const normalized = normalizeDigestAlgorithm(config.digestAlgorithm);
|
|
843
|
+
if (allowedDigestAlgorithms) {
|
|
844
|
+
if (!allowedDigestAlgorithms.map(normalizeDigestAlgorithm).includes(normalized)) throw new APIError("BAD_REQUEST", {
|
|
845
|
+
message: `SAML digest algorithm not in allow-list: ${config.digestAlgorithm}`,
|
|
846
|
+
code: "SAML_ALGORITHM_NOT_ALLOWED"
|
|
847
|
+
});
|
|
848
|
+
} else if (DEPRECATED_DIGEST_ALGORITHMS.includes(normalized)) handleDeprecatedAlgorithm(`SAML config uses deprecated digest algorithm: ${config.digestAlgorithm}. Consider using SHA-256 or stronger.`, onDeprecated, "SAML_DEPRECATED_CONFIG_ALGORITHM");
|
|
849
|
+
else if (!SECURE_DIGEST_ALGORITHMS.includes(normalized)) throw new APIError("BAD_REQUEST", {
|
|
850
|
+
message: `SAML digest algorithm not recognized: ${config.digestAlgorithm}`,
|
|
851
|
+
code: "SAML_UNKNOWN_ALGORITHM"
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
794
855
|
|
|
795
856
|
//#endregion
|
|
796
857
|
//#region src/utils.ts
|
|
@@ -1252,6 +1313,10 @@ const registerSSOProvider = (options) => {
|
|
|
1252
1313
|
overrideUserInfo: ctx.body.overrideUserInfo || options?.defaultOverrideUserInfo || false
|
|
1253
1314
|
});
|
|
1254
1315
|
};
|
|
1316
|
+
if (body.samlConfig) validateConfigAlgorithms({
|
|
1317
|
+
signatureAlgorithm: body.samlConfig.signatureAlgorithm,
|
|
1318
|
+
digestAlgorithm: body.samlConfig.digestAlgorithm
|
|
1319
|
+
}, options?.saml?.algorithms);
|
|
1255
1320
|
const provider = await ctx.context.adapter.create({
|
|
1256
1321
|
model: "ssoProvider",
|
|
1257
1322
|
data: {
|
|
@@ -1297,14 +1362,15 @@ const registerSSOProvider = (options) => {
|
|
|
1297
1362
|
}
|
|
1298
1363
|
});
|
|
1299
1364
|
}
|
|
1300
|
-
|
|
1365
|
+
const result = {
|
|
1301
1366
|
...provider,
|
|
1302
1367
|
oidcConfig: safeJsonParse(provider.oidcConfig),
|
|
1303
1368
|
samlConfig: safeJsonParse(provider.samlConfig),
|
|
1304
1369
|
redirectURI: `${ctx.context.baseURL}/sso/callback/${provider.providerId}`,
|
|
1305
1370
|
...options?.domainVerification?.enabled ? { domainVerified } : {},
|
|
1306
1371
|
...options?.domainVerification?.enabled ? { domainVerificationToken } : {}
|
|
1307
|
-
}
|
|
1372
|
+
};
|
|
1373
|
+
return ctx.json(result);
|
|
1308
1374
|
});
|
|
1309
1375
|
};
|
|
1310
1376
|
const signInSSOBodySchema = z.object({
|
|
@@ -1782,7 +1848,7 @@ const callbackSSOSAML = (options) => {
|
|
|
1782
1848
|
} catch (error) {
|
|
1783
1849
|
ctx.context.logger.error("SAML response validation failed", {
|
|
1784
1850
|
error,
|
|
1785
|
-
decodedResponse:
|
|
1851
|
+
decodedResponse: new TextDecoder().decode(base64.decode(SAMLResponse))
|
|
1786
1852
|
});
|
|
1787
1853
|
throw new APIError("BAD_REQUEST", {
|
|
1788
1854
|
message: "Invalid SAML response",
|
|
@@ -2016,7 +2082,7 @@ const acsEndpoint = (options) => {
|
|
|
2016
2082
|
} catch (error) {
|
|
2017
2083
|
ctx.context.logger.error("SAML response validation failed", {
|
|
2018
2084
|
error,
|
|
2019
|
-
decodedResponse:
|
|
2085
|
+
decodedResponse: new TextDecoder().decode(base64.decode(SAMLResponse))
|
|
2020
2086
|
});
|
|
2021
2087
|
throw new APIError("BAD_REQUEST", {
|
|
2022
2088
|
message: "Invalid SAML response",
|
|
@@ -2067,7 +2133,7 @@ const acsEndpoint = (options) => {
|
|
|
2067
2133
|
throw ctx.redirect(`${redirectUrl}?error=unsolicited_response&error_description=IdP-initiated+SSO+not+allowed`);
|
|
2068
2134
|
}
|
|
2069
2135
|
}
|
|
2070
|
-
const assertionIdAcs = extractAssertionId(
|
|
2136
|
+
const assertionIdAcs = extractAssertionId(new TextDecoder().decode(base64.decode(SAMLResponse)));
|
|
2071
2137
|
if (assertionIdAcs) {
|
|
2072
2138
|
const issuer = idp.entityMeta.getEntityID();
|
|
2073
2139
|
const conditions = extract.conditions;
|
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.
|
|
4
|
+
"version": "1.4.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.mts",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
+
"@better-auth/utils": "0.3.0",
|
|
55
56
|
"@better-fetch/fetch": "1.1.21",
|
|
56
57
|
"fast-xml-parser": "^5.2.5",
|
|
57
58
|
"jose": "^6.1.0",
|
|
@@ -66,10 +67,10 @@
|
|
|
66
67
|
"express": "^5.1.0",
|
|
67
68
|
"oauth2-mock-server": "^8.2.0",
|
|
68
69
|
"tsdown": "^0.17.2",
|
|
69
|
-
"better-auth": "1.4.
|
|
70
|
+
"better-auth": "1.4.9"
|
|
70
71
|
},
|
|
71
72
|
"peerDependencies": {
|
|
72
|
-
"better-auth": "1.4.
|
|
73
|
+
"better-auth": "1.4.9"
|
|
73
74
|
},
|
|
74
75
|
"scripts": {
|
|
75
76
|
"test": "vitest",
|
package/src/routes/sso.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { base64 } from "@better-auth/utils/base64";
|
|
1
2
|
import { BetterFetchError, betterFetch } from "@better-fetch/fetch";
|
|
2
3
|
import type { User, Verification } from "better-auth";
|
|
3
4
|
import {
|
|
@@ -45,7 +46,7 @@ import {
|
|
|
45
46
|
discoverOIDCConfig,
|
|
46
47
|
mapDiscoveryErrorToAPIError,
|
|
47
48
|
} from "../oidc";
|
|
48
|
-
import { validateSAMLAlgorithms } from "../saml";
|
|
49
|
+
import { validateConfigAlgorithms, validateSAMLAlgorithms } from "../saml";
|
|
49
50
|
import type { OIDCConfig, SAMLConfig, SSOOptions, SSOProvider } from "../types";
|
|
50
51
|
import { safeJsonParse, validateEmailDomain } from "../utils";
|
|
51
52
|
|
|
@@ -780,6 +781,16 @@ export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
|
|
|
780
781
|
});
|
|
781
782
|
};
|
|
782
783
|
|
|
784
|
+
if (body.samlConfig) {
|
|
785
|
+
validateConfigAlgorithms(
|
|
786
|
+
{
|
|
787
|
+
signatureAlgorithm: body.samlConfig.signatureAlgorithm,
|
|
788
|
+
digestAlgorithm: body.samlConfig.digestAlgorithm,
|
|
789
|
+
},
|
|
790
|
+
options?.saml?.algorithms,
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
783
794
|
const provider = await ctx.context.adapter.create<
|
|
784
795
|
Record<string, any>,
|
|
785
796
|
SSOProvider<O>
|
|
@@ -836,14 +847,20 @@ export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
|
|
|
836
847
|
});
|
|
837
848
|
}
|
|
838
849
|
|
|
850
|
+
type SSOProviderResponse = {
|
|
851
|
+
redirectURI: string;
|
|
852
|
+
oidcConfig: OIDCConfig | null;
|
|
853
|
+
samlConfig: SAMLConfig | null;
|
|
854
|
+
} & Omit<SSOProvider<O>, "oidcConfig" | "samlConfig">;
|
|
855
|
+
|
|
839
856
|
type SSOProviderReturn = O["domainVerification"] extends { enabled: true }
|
|
840
|
-
? {
|
|
857
|
+
? SSOProviderResponse & {
|
|
841
858
|
domainVerified: boolean;
|
|
842
859
|
domainVerificationToken: string;
|
|
843
|
-
}
|
|
844
|
-
:
|
|
860
|
+
}
|
|
861
|
+
: SSOProviderResponse;
|
|
845
862
|
|
|
846
|
-
|
|
863
|
+
const result = {
|
|
847
864
|
...provider,
|
|
848
865
|
oidcConfig: safeJsonParse<OIDCConfig>(
|
|
849
866
|
provider.oidcConfig as unknown as string,
|
|
@@ -856,7 +873,9 @@ export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
|
|
|
856
873
|
...(options?.domainVerification?.enabled
|
|
857
874
|
? { domainVerificationToken }
|
|
858
875
|
: {}),
|
|
859
|
-
}
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
return ctx.json(result as SSOProviderReturn);
|
|
860
879
|
},
|
|
861
880
|
);
|
|
862
881
|
};
|
|
@@ -1807,8 +1826,8 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
|
|
|
1807
1826
|
} catch (error) {
|
|
1808
1827
|
ctx.context.logger.error("SAML response validation failed", {
|
|
1809
1828
|
error,
|
|
1810
|
-
decodedResponse:
|
|
1811
|
-
|
|
1829
|
+
decodedResponse: new TextDecoder().decode(
|
|
1830
|
+
base64.decode(SAMLResponse),
|
|
1812
1831
|
),
|
|
1813
1832
|
});
|
|
1814
1833
|
throw new APIError("BAD_REQUEST", {
|
|
@@ -2237,8 +2256,8 @@ export const acsEndpoint = (options?: SSOOptions) => {
|
|
|
2237
2256
|
} catch (error) {
|
|
2238
2257
|
ctx.context.logger.error("SAML response validation failed", {
|
|
2239
2258
|
error,
|
|
2240
|
-
decodedResponse:
|
|
2241
|
-
|
|
2259
|
+
decodedResponse: new TextDecoder().decode(
|
|
2260
|
+
base64.decode(SAMLResponse),
|
|
2242
2261
|
),
|
|
2243
2262
|
});
|
|
2244
2263
|
throw new APIError("BAD_REQUEST", {
|
|
@@ -2334,8 +2353,8 @@ export const acsEndpoint = (options?: SSOOptions) => {
|
|
|
2334
2353
|
}
|
|
2335
2354
|
|
|
2336
2355
|
// Assertion Replay Protection
|
|
2337
|
-
const samlContentAcs =
|
|
2338
|
-
|
|
2356
|
+
const samlContentAcs = new TextDecoder().decode(
|
|
2357
|
+
base64.decode(SAMLResponse),
|
|
2339
2358
|
);
|
|
2340
2359
|
const assertionIdAcs = extractAssertionId(samlContentAcs);
|
|
2341
2360
|
|
|
@@ -203,3 +203,247 @@ describe("algorithm constants", () => {
|
|
|
203
203
|
);
|
|
204
204
|
});
|
|
205
205
|
});
|
|
206
|
+
|
|
207
|
+
describe("validateConfigAlgorithms", () => {
|
|
208
|
+
afterEach(() => {
|
|
209
|
+
vi.restoreAllMocks();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("signature algorithm validation", () => {
|
|
213
|
+
it("should accept secure signature algorithms", () => {
|
|
214
|
+
expect(() =>
|
|
215
|
+
alg.validateConfigAlgorithms({
|
|
216
|
+
signatureAlgorithm: alg.SignatureAlgorithm.RSA_SHA256,
|
|
217
|
+
}),
|
|
218
|
+
).not.toThrow();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should warn by default for deprecated signature algorithms", () => {
|
|
222
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
223
|
+
|
|
224
|
+
expect(() =>
|
|
225
|
+
alg.validateConfigAlgorithms({
|
|
226
|
+
signatureAlgorithm: alg.SignatureAlgorithm.RSA_SHA1,
|
|
227
|
+
}),
|
|
228
|
+
).not.toThrow();
|
|
229
|
+
|
|
230
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
231
|
+
expect.stringContaining("SAML Security Warning"),
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should reject deprecated signature with onDeprecated: reject", () => {
|
|
236
|
+
expect(() =>
|
|
237
|
+
alg.validateConfigAlgorithms(
|
|
238
|
+
{ signatureAlgorithm: alg.SignatureAlgorithm.RSA_SHA1 },
|
|
239
|
+
{ onDeprecated: "reject" },
|
|
240
|
+
),
|
|
241
|
+
).toThrow(/deprecated/i);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should silently allow deprecated with onDeprecated: allow", () => {
|
|
245
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
246
|
+
|
|
247
|
+
expect(() =>
|
|
248
|
+
alg.validateConfigAlgorithms(
|
|
249
|
+
{ signatureAlgorithm: alg.SignatureAlgorithm.RSA_SHA1 },
|
|
250
|
+
{ onDeprecated: "allow" },
|
|
251
|
+
),
|
|
252
|
+
).not.toThrow();
|
|
253
|
+
|
|
254
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should enforce custom signature allow-list", () => {
|
|
258
|
+
expect(() =>
|
|
259
|
+
alg.validateConfigAlgorithms(
|
|
260
|
+
{ signatureAlgorithm: alg.SignatureAlgorithm.RSA_SHA256 },
|
|
261
|
+
{ allowedSignatureAlgorithms: [alg.SignatureAlgorithm.RSA_SHA512] },
|
|
262
|
+
),
|
|
263
|
+
).toThrow(/not in allow-list/i);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should reject unknown signature algorithms", () => {
|
|
267
|
+
expect(() =>
|
|
268
|
+
alg.validateConfigAlgorithms({
|
|
269
|
+
signatureAlgorithm: "http://example.com/unknown-algo",
|
|
270
|
+
}),
|
|
271
|
+
).toThrow(/not recognized/i);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should pass undefined signatureAlgorithm without error", () => {
|
|
275
|
+
expect(() => alg.validateConfigAlgorithms({})).not.toThrow();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should accept short-form signature algorithm names", () => {
|
|
279
|
+
expect(() =>
|
|
280
|
+
alg.validateConfigAlgorithms({
|
|
281
|
+
signatureAlgorithm: "rsa-sha256",
|
|
282
|
+
}),
|
|
283
|
+
).not.toThrow();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("should accept digest-style short-form for signature (backward compat)", () => {
|
|
287
|
+
expect(() =>
|
|
288
|
+
alg.validateConfigAlgorithms({
|
|
289
|
+
signatureAlgorithm: "sha256",
|
|
290
|
+
}),
|
|
291
|
+
).not.toThrow();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("should reject typos in short-form signature algorithm names", () => {
|
|
295
|
+
expect(() =>
|
|
296
|
+
alg.validateConfigAlgorithms({
|
|
297
|
+
signatureAlgorithm: "rsa-sha257",
|
|
298
|
+
}),
|
|
299
|
+
).toThrow(/not recognized/i);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("should warn for deprecated short-form signature algorithms", () => {
|
|
303
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
304
|
+
|
|
305
|
+
expect(() =>
|
|
306
|
+
alg.validateConfigAlgorithms({
|
|
307
|
+
signatureAlgorithm: "rsa-sha1",
|
|
308
|
+
}),
|
|
309
|
+
).not.toThrow();
|
|
310
|
+
|
|
311
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
312
|
+
expect.stringContaining("SAML Security Warning"),
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("should support short-form names in signature allow-list", () => {
|
|
317
|
+
expect(() =>
|
|
318
|
+
alg.validateConfigAlgorithms(
|
|
319
|
+
{ signatureAlgorithm: "rsa-sha256" },
|
|
320
|
+
{ allowedSignatureAlgorithms: ["rsa-sha256", "rsa-sha512"] },
|
|
321
|
+
),
|
|
322
|
+
).not.toThrow();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe("digest algorithm validation", () => {
|
|
327
|
+
it("should accept secure digest algorithms", () => {
|
|
328
|
+
expect(() =>
|
|
329
|
+
alg.validateConfigAlgorithms({
|
|
330
|
+
digestAlgorithm: alg.DigestAlgorithm.SHA256,
|
|
331
|
+
}),
|
|
332
|
+
).not.toThrow();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("should warn by default for deprecated digest algorithms", () => {
|
|
336
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
337
|
+
|
|
338
|
+
expect(() =>
|
|
339
|
+
alg.validateConfigAlgorithms({
|
|
340
|
+
digestAlgorithm: alg.DigestAlgorithm.SHA1,
|
|
341
|
+
}),
|
|
342
|
+
).not.toThrow();
|
|
343
|
+
|
|
344
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
345
|
+
expect.stringContaining("SAML Security Warning"),
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("should reject deprecated digest with onDeprecated: reject", () => {
|
|
350
|
+
expect(() =>
|
|
351
|
+
alg.validateConfigAlgorithms(
|
|
352
|
+
{ digestAlgorithm: alg.DigestAlgorithm.SHA1 },
|
|
353
|
+
{ onDeprecated: "reject" },
|
|
354
|
+
),
|
|
355
|
+
).toThrow(/deprecated/i);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("should enforce custom digest allow-list", () => {
|
|
359
|
+
expect(() =>
|
|
360
|
+
alg.validateConfigAlgorithms(
|
|
361
|
+
{ digestAlgorithm: alg.DigestAlgorithm.SHA256 },
|
|
362
|
+
{ allowedDigestAlgorithms: [alg.DigestAlgorithm.SHA512] },
|
|
363
|
+
),
|
|
364
|
+
).toThrow(/not in allow-list/i);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("should reject unknown digest algorithms", () => {
|
|
368
|
+
expect(() =>
|
|
369
|
+
alg.validateConfigAlgorithms({
|
|
370
|
+
digestAlgorithm: "http://example.com/unknown-digest",
|
|
371
|
+
}),
|
|
372
|
+
).toThrow(/not recognized/i);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("should accept short-form digest algorithm names", () => {
|
|
376
|
+
expect(() =>
|
|
377
|
+
alg.validateConfigAlgorithms({
|
|
378
|
+
digestAlgorithm: "sha256",
|
|
379
|
+
}),
|
|
380
|
+
).not.toThrow();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("should reject typos in short-form digest algorithm names", () => {
|
|
384
|
+
expect(() =>
|
|
385
|
+
alg.validateConfigAlgorithms({
|
|
386
|
+
digestAlgorithm: "sha257",
|
|
387
|
+
}),
|
|
388
|
+
).toThrow(/not recognized/i);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("should warn for deprecated short-form digest algorithms", () => {
|
|
392
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
393
|
+
|
|
394
|
+
expect(() =>
|
|
395
|
+
alg.validateConfigAlgorithms({
|
|
396
|
+
digestAlgorithm: "sha1",
|
|
397
|
+
}),
|
|
398
|
+
).not.toThrow();
|
|
399
|
+
|
|
400
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
401
|
+
expect.stringContaining("SAML Security Warning"),
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("should support short-form names in digest allow-list", () => {
|
|
406
|
+
expect(() =>
|
|
407
|
+
alg.validateConfigAlgorithms(
|
|
408
|
+
{ digestAlgorithm: "sha256" },
|
|
409
|
+
{ allowedDigestAlgorithms: ["sha256", "sha512"] },
|
|
410
|
+
),
|
|
411
|
+
).not.toThrow();
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe("combined validation", () => {
|
|
416
|
+
it("should validate both signature and digest algorithms", () => {
|
|
417
|
+
expect(() =>
|
|
418
|
+
alg.validateConfigAlgorithms({
|
|
419
|
+
signatureAlgorithm: alg.SignatureAlgorithm.RSA_SHA256,
|
|
420
|
+
digestAlgorithm: alg.DigestAlgorithm.SHA256,
|
|
421
|
+
}),
|
|
422
|
+
).not.toThrow();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("should reject if signature is deprecated even if digest is secure", () => {
|
|
426
|
+
expect(() =>
|
|
427
|
+
alg.validateConfigAlgorithms(
|
|
428
|
+
{
|
|
429
|
+
signatureAlgorithm: alg.SignatureAlgorithm.RSA_SHA1,
|
|
430
|
+
digestAlgorithm: alg.DigestAlgorithm.SHA256,
|
|
431
|
+
},
|
|
432
|
+
{ onDeprecated: "reject" },
|
|
433
|
+
),
|
|
434
|
+
).toThrow(/deprecated/i);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("should reject if digest is deprecated even if signature is secure", () => {
|
|
438
|
+
expect(() =>
|
|
439
|
+
alg.validateConfigAlgorithms(
|
|
440
|
+
{
|
|
441
|
+
signatureAlgorithm: alg.SignatureAlgorithm.RSA_SHA256,
|
|
442
|
+
digestAlgorithm: alg.DigestAlgorithm.SHA1,
|
|
443
|
+
},
|
|
444
|
+
{ onDeprecated: "reject" },
|
|
445
|
+
),
|
|
446
|
+
).toThrow(/deprecated/i);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
});
|
package/src/saml/algorithms.ts
CHANGED
|
@@ -46,6 +46,8 @@ const DEPRECATED_DATA_ENCRYPTION_ALGORITHMS: readonly string[] = [
|
|
|
46
46
|
DataEncryptionAlgorithm.TRIPLEDES_CBC,
|
|
47
47
|
];
|
|
48
48
|
|
|
49
|
+
const DEPRECATED_DIGEST_ALGORITHMS: readonly string[] = [DigestAlgorithm.SHA1];
|
|
50
|
+
|
|
49
51
|
const SECURE_SIGNATURE_ALGORITHMS: readonly string[] = [
|
|
50
52
|
SignatureAlgorithm.RSA_SHA256,
|
|
51
53
|
SignatureAlgorithm.RSA_SHA384,
|
|
@@ -55,6 +57,41 @@ const SECURE_SIGNATURE_ALGORITHMS: readonly string[] = [
|
|
|
55
57
|
SignatureAlgorithm.ECDSA_SHA512,
|
|
56
58
|
];
|
|
57
59
|
|
|
60
|
+
const SECURE_DIGEST_ALGORITHMS: readonly string[] = [
|
|
61
|
+
DigestAlgorithm.SHA256,
|
|
62
|
+
DigestAlgorithm.SHA384,
|
|
63
|
+
DigestAlgorithm.SHA512,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const SHORT_FORM_SIGNATURE_TO_URI: Record<string, string> = {
|
|
67
|
+
sha1: SignatureAlgorithm.RSA_SHA1,
|
|
68
|
+
sha256: SignatureAlgorithm.RSA_SHA256,
|
|
69
|
+
sha384: SignatureAlgorithm.RSA_SHA384,
|
|
70
|
+
sha512: SignatureAlgorithm.RSA_SHA512,
|
|
71
|
+
"rsa-sha1": SignatureAlgorithm.RSA_SHA1,
|
|
72
|
+
"rsa-sha256": SignatureAlgorithm.RSA_SHA256,
|
|
73
|
+
"rsa-sha384": SignatureAlgorithm.RSA_SHA384,
|
|
74
|
+
"rsa-sha512": SignatureAlgorithm.RSA_SHA512,
|
|
75
|
+
"ecdsa-sha256": SignatureAlgorithm.ECDSA_SHA256,
|
|
76
|
+
"ecdsa-sha384": SignatureAlgorithm.ECDSA_SHA384,
|
|
77
|
+
"ecdsa-sha512": SignatureAlgorithm.ECDSA_SHA512,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const SHORT_FORM_DIGEST_TO_URI: Record<string, string> = {
|
|
81
|
+
sha1: DigestAlgorithm.SHA1,
|
|
82
|
+
sha256: DigestAlgorithm.SHA256,
|
|
83
|
+
sha384: DigestAlgorithm.SHA384,
|
|
84
|
+
sha512: DigestAlgorithm.SHA512,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function normalizeSignatureAlgorithm(alg: string): string {
|
|
88
|
+
return SHORT_FORM_SIGNATURE_TO_URI[alg.toLowerCase()] ?? alg;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeDigestAlgorithm(alg: string): string {
|
|
92
|
+
return SHORT_FORM_DIGEST_TO_URI[alg.toLowerCase()] ?? alg;
|
|
93
|
+
}
|
|
94
|
+
|
|
58
95
|
export type DeprecatedAlgorithmBehavior = "reject" | "warn" | "allow";
|
|
59
96
|
|
|
60
97
|
export interface AlgorithmValidationOptions {
|
|
@@ -257,3 +294,75 @@ export function validateSAMLAlgorithms(
|
|
|
257
294
|
validateEncryptionAlgorithms(encAlgs, options);
|
|
258
295
|
}
|
|
259
296
|
}
|
|
297
|
+
|
|
298
|
+
export interface ConfigAlgorithmValidationOptions {
|
|
299
|
+
onDeprecated?: DeprecatedAlgorithmBehavior;
|
|
300
|
+
allowedSignatureAlgorithms?: string[];
|
|
301
|
+
allowedDigestAlgorithms?: string[];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function validateConfigAlgorithms(
|
|
305
|
+
config: {
|
|
306
|
+
signatureAlgorithm?: string | undefined;
|
|
307
|
+
digestAlgorithm?: string | undefined;
|
|
308
|
+
},
|
|
309
|
+
options: ConfigAlgorithmValidationOptions = {},
|
|
310
|
+
): void {
|
|
311
|
+
const {
|
|
312
|
+
onDeprecated = "warn",
|
|
313
|
+
allowedSignatureAlgorithms,
|
|
314
|
+
allowedDigestAlgorithms,
|
|
315
|
+
} = options;
|
|
316
|
+
|
|
317
|
+
if (config.signatureAlgorithm) {
|
|
318
|
+
const normalized = normalizeSignatureAlgorithm(config.signatureAlgorithm);
|
|
319
|
+
if (allowedSignatureAlgorithms) {
|
|
320
|
+
const normalizedAllowList = allowedSignatureAlgorithms.map(
|
|
321
|
+
normalizeSignatureAlgorithm,
|
|
322
|
+
);
|
|
323
|
+
if (!normalizedAllowList.includes(normalized)) {
|
|
324
|
+
throw new APIError("BAD_REQUEST", {
|
|
325
|
+
message: `SAML signature algorithm not in allow-list: ${config.signatureAlgorithm}`,
|
|
326
|
+
code: "SAML_ALGORITHM_NOT_ALLOWED",
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
} else if (DEPRECATED_SIGNATURE_ALGORITHMS.includes(normalized)) {
|
|
330
|
+
handleDeprecatedAlgorithm(
|
|
331
|
+
`SAML config uses deprecated signature algorithm: ${config.signatureAlgorithm}. Consider using SHA-256 or stronger.`,
|
|
332
|
+
onDeprecated,
|
|
333
|
+
"SAML_DEPRECATED_CONFIG_ALGORITHM",
|
|
334
|
+
);
|
|
335
|
+
} else if (!SECURE_SIGNATURE_ALGORITHMS.includes(normalized)) {
|
|
336
|
+
throw new APIError("BAD_REQUEST", {
|
|
337
|
+
message: `SAML signature algorithm not recognized: ${config.signatureAlgorithm}`,
|
|
338
|
+
code: "SAML_UNKNOWN_ALGORITHM",
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (config.digestAlgorithm) {
|
|
344
|
+
const normalized = normalizeDigestAlgorithm(config.digestAlgorithm);
|
|
345
|
+
if (allowedDigestAlgorithms) {
|
|
346
|
+
const normalizedAllowList = allowedDigestAlgorithms.map(
|
|
347
|
+
normalizeDigestAlgorithm,
|
|
348
|
+
);
|
|
349
|
+
if (!normalizedAllowList.includes(normalized)) {
|
|
350
|
+
throw new APIError("BAD_REQUEST", {
|
|
351
|
+
message: `SAML digest algorithm not in allow-list: ${config.digestAlgorithm}`,
|
|
352
|
+
code: "SAML_ALGORITHM_NOT_ALLOWED",
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
} else if (DEPRECATED_DIGEST_ALGORITHMS.includes(normalized)) {
|
|
356
|
+
handleDeprecatedAlgorithm(
|
|
357
|
+
`SAML config uses deprecated digest algorithm: ${config.digestAlgorithm}. Consider using SHA-256 or stronger.`,
|
|
358
|
+
onDeprecated,
|
|
359
|
+
"SAML_DEPRECATED_CONFIG_ALGORITHM",
|
|
360
|
+
);
|
|
361
|
+
} else if (!SECURE_DIGEST_ALGORITHMS.includes(normalized)) {
|
|
362
|
+
throw new APIError("BAD_REQUEST", {
|
|
363
|
+
message: `SAML digest algorithm not recognized: ${config.digestAlgorithm}`,
|
|
364
|
+
code: "SAML_UNKNOWN_ALGORITHM",
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
package/src/saml/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export {
|
|
2
2
|
type AlgorithmValidationOptions,
|
|
3
|
+
type ConfigAlgorithmValidationOptions,
|
|
3
4
|
DataEncryptionAlgorithm,
|
|
4
5
|
type DeprecatedAlgorithmBehavior,
|
|
5
6
|
DigestAlgorithm,
|
|
6
7
|
KeyEncryptionAlgorithm,
|
|
7
8
|
SignatureAlgorithm,
|
|
9
|
+
validateConfigAlgorithms,
|
|
8
10
|
validateSAMLAlgorithms,
|
|
9
11
|
} from "./algorithms";
|