@better-auth/sso 1.4.8 → 1.4.10-beta.1
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-ZWFEs7WQ.d.mts → index-CvpS40sl.d.mts} +6 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +64 -0
- package/package.json +3 -3
- package/src/index.ts +1 -1
- package/src/linking/org-assignment.ts +3 -21
- package/src/routes/sso.ts +11 -1
- package/src/saml/algorithms.test.ts +244 -0
- package/src/saml/algorithms.ts +109 -0
- package/src/saml/index.ts +2 -0
- package/src/types.ts +6 -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.10-beta.1 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 [32m16051ms[39m
|
package/dist/client.d.mts
CHANGED
|
@@ -257,7 +257,13 @@ interface SSOOptions {
|
|
|
257
257
|
*
|
|
258
258
|
* If you want to allow account linking for specific trusted providers, enable the `accountLinking` option in your auth config and specify those
|
|
259
259
|
* providers in the `trustedProviders` list.
|
|
260
|
+
*
|
|
260
261
|
* @default false
|
|
262
|
+
*
|
|
263
|
+
* @deprecated This option is discouraged for new projects. Relying on provider-level `email_verified` is a weaker
|
|
264
|
+
* trust signal compared to using `trustedProviders` in `accountLinking` or enabling `domainVerification` for SSO.
|
|
265
|
+
* Existing configurations will continue to work, but new integrations should use explicit trust mechanisms.
|
|
266
|
+
* This option may be removed in a future major version.
|
|
261
267
|
*/
|
|
262
268
|
trustEmailVerified?: boolean | undefined;
|
|
263
269
|
/**
|
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
|
@@ -687,6 +687,7 @@ const DataEncryptionAlgorithm = {
|
|
|
687
687
|
const DEPRECATED_SIGNATURE_ALGORITHMS = [SignatureAlgorithm.RSA_SHA1];
|
|
688
688
|
const DEPRECATED_KEY_ENCRYPTION_ALGORITHMS = [KeyEncryptionAlgorithm.RSA_1_5];
|
|
689
689
|
const DEPRECATED_DATA_ENCRYPTION_ALGORITHMS = [DataEncryptionAlgorithm.TRIPLEDES_CBC];
|
|
690
|
+
const DEPRECATED_DIGEST_ALGORITHMS = [DigestAlgorithm.SHA1];
|
|
690
691
|
const SECURE_SIGNATURE_ALGORITHMS = [
|
|
691
692
|
SignatureAlgorithm.RSA_SHA256,
|
|
692
693
|
SignatureAlgorithm.RSA_SHA384,
|
|
@@ -695,6 +696,36 @@ const SECURE_SIGNATURE_ALGORITHMS = [
|
|
|
695
696
|
SignatureAlgorithm.ECDSA_SHA384,
|
|
696
697
|
SignatureAlgorithm.ECDSA_SHA512
|
|
697
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
|
+
}
|
|
698
729
|
const xmlParser = new XMLParser({
|
|
699
730
|
ignoreAttributes: false,
|
|
700
731
|
attributeNamePrefix: "@_",
|
|
@@ -792,6 +823,35 @@ function validateSAMLAlgorithms(response, options) {
|
|
|
792
823
|
validateSignatureAlgorithm(response.sigAlg, options);
|
|
793
824
|
if (hasEncryptedAssertion(response.samlContent)) validateEncryptionAlgorithms(extractEncryptionAlgorithms(response.samlContent), options);
|
|
794
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
|
+
}
|
|
795
855
|
|
|
796
856
|
//#endregion
|
|
797
857
|
//#region src/utils.ts
|
|
@@ -1253,6 +1313,10 @@ const registerSSOProvider = (options) => {
|
|
|
1253
1313
|
overrideUserInfo: ctx.body.overrideUserInfo || options?.defaultOverrideUserInfo || false
|
|
1254
1314
|
});
|
|
1255
1315
|
};
|
|
1316
|
+
if (body.samlConfig) validateConfigAlgorithms({
|
|
1317
|
+
signatureAlgorithm: body.samlConfig.signatureAlgorithm,
|
|
1318
|
+
digestAlgorithm: body.samlConfig.digestAlgorithm
|
|
1319
|
+
}, options?.saml?.algorithms);
|
|
1256
1320
|
const provider = await ctx.context.adapter.create({
|
|
1257
1321
|
model: "ssoProvider",
|
|
1258
1322
|
data: {
|
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.10-beta.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.mts",
|
|
@@ -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.
|
|
70
|
+
"better-auth": "1.4.10-beta.1"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"better-auth": "1.4.
|
|
73
|
+
"better-auth": "1.4.10-beta.1"
|
|
74
74
|
},
|
|
75
75
|
"scripts": {
|
|
76
76
|
"test": "vitest",
|
package/src/index.ts
CHANGED
|
@@ -153,7 +153,7 @@ export function sso<O extends SSOOptions>(options?: O | undefined): any {
|
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
await assignOrganizationByDomain(ctx
|
|
156
|
+
await assignOrganizationByDomain(ctx, {
|
|
157
157
|
user: newSession.user,
|
|
158
158
|
provisioningOptions: options?.organizationProvisioning,
|
|
159
159
|
domainVerification: options?.domainVerification,
|
|
@@ -1,25 +1,7 @@
|
|
|
1
|
-
import type { OAuth2Tokens, User } from "better-auth";
|
|
1
|
+
import type { GenericEndpointContext, OAuth2Tokens, User } from "better-auth";
|
|
2
2
|
import type { SSOOptions, SSOProvider } from "../types";
|
|
3
3
|
import type { NormalizedSSOProfile } from "./types";
|
|
4
4
|
|
|
5
|
-
interface EndpointContext {
|
|
6
|
-
context: {
|
|
7
|
-
options: {
|
|
8
|
-
plugins?: Array<{ id: string }>;
|
|
9
|
-
};
|
|
10
|
-
adapter: {
|
|
11
|
-
findOne: <T>(options: {
|
|
12
|
-
model: string;
|
|
13
|
-
where: Array<{ field: string; value: unknown }>;
|
|
14
|
-
}) => Promise<T | null>;
|
|
15
|
-
create: (options: {
|
|
16
|
-
model: string;
|
|
17
|
-
data: Record<string, unknown>;
|
|
18
|
-
}) => Promise<unknown>;
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
5
|
export interface OrganizationProvisioningOptions {
|
|
24
6
|
disabled?: boolean;
|
|
25
7
|
defaultRole?: "member" | "admin";
|
|
@@ -44,7 +26,7 @@ export interface AssignOrganizationFromProviderOptions {
|
|
|
44
26
|
* Used in SSO flows (OIDC, SAML) where the provider is already linked to an org.
|
|
45
27
|
*/
|
|
46
28
|
export async function assignOrganizationFromProvider(
|
|
47
|
-
ctx:
|
|
29
|
+
ctx: GenericEndpointContext,
|
|
48
30
|
options: AssignOrganizationFromProviderOptions,
|
|
49
31
|
): Promise<void> {
|
|
50
32
|
const { user, profile, provider, token, provisioningOptions } = options;
|
|
@@ -114,7 +96,7 @@ export interface AssignOrganizationByDomainOptions {
|
|
|
114
96
|
* (e.g., Google OAuth with @acme.com email gets added to Acme's org).
|
|
115
97
|
*/
|
|
116
98
|
export async function assignOrganizationByDomain(
|
|
117
|
-
ctx:
|
|
99
|
+
ctx: GenericEndpointContext,
|
|
118
100
|
options: AssignOrganizationByDomainOptions,
|
|
119
101
|
): Promise<void> {
|
|
120
102
|
const { user, provisioningOptions, domainVerification } = options;
|
package/src/routes/sso.ts
CHANGED
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
discoverOIDCConfig,
|
|
47
47
|
mapDiscoveryErrorToAPIError,
|
|
48
48
|
} from "../oidc";
|
|
49
|
-
import { validateSAMLAlgorithms } from "../saml";
|
|
49
|
+
import { validateConfigAlgorithms, validateSAMLAlgorithms } from "../saml";
|
|
50
50
|
import type { OIDCConfig, SAMLConfig, SSOOptions, SSOProvider } from "../types";
|
|
51
51
|
import { safeJsonParse, validateEmailDomain } from "../utils";
|
|
52
52
|
|
|
@@ -781,6 +781,16 @@ export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
|
|
|
781
781
|
});
|
|
782
782
|
};
|
|
783
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
|
+
|
|
784
794
|
const provider = await ctx.context.adapter.create<
|
|
785
795
|
Record<string, any>,
|
|
786
796
|
SSOProvider<O>
|
|
@@ -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";
|
package/src/types.ts
CHANGED
|
@@ -231,7 +231,13 @@ export interface SSOOptions {
|
|
|
231
231
|
*
|
|
232
232
|
* If you want to allow account linking for specific trusted providers, enable the `accountLinking` option in your auth config and specify those
|
|
233
233
|
* providers in the `trustedProviders` list.
|
|
234
|
+
*
|
|
234
235
|
* @default false
|
|
236
|
+
*
|
|
237
|
+
* @deprecated This option is discouraged for new projects. Relying on provider-level `email_verified` is a weaker
|
|
238
|
+
* trust signal compared to using `trustedProviders` in `accountLinking` or enabling `domainVerification` for SSO.
|
|
239
|
+
* Existing configurations will continue to work, but new integrations should use explicit trust mechanisms.
|
|
240
|
+
* This option may be removed in a future major version.
|
|
235
241
|
*/
|
|
236
242
|
trustEmailVerified?: boolean | undefined;
|
|
237
243
|
/**
|