@better-auth/sso 1.4.8 → 1.5.0-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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @better-auth/sso@1.4.8 build /home/runner/work/better-auth/better-auth/packages/sso
2
+ > @better-auth/sso@1.5.0-beta.1 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 92.73 kB │ gzip: 18.16 kB
10
+ ℹ dist/index.mjs 95.91 kB │ gzip: 18.60 kB
11
11
  ℹ dist/client.mjs  0.15 kB │ gzip: 0.14 kB
12
12
  ℹ dist/index.d.mts  1.48 kB │ gzip: 0.51 kB
13
- ℹ dist/client.d.mts  0.49 kB │ gzip: 0.30 kB
14
- ℹ dist/index-ZWFEs7WQ.d.mts 42.70 kB │ gzip: 8.67 kB
15
- ℹ 5 files, total: 137.55 kB
16
- ✔ Build complete in 15873ms
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 15982ms
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as SSOPlugin } from "./index-ZWFEs7WQ.mjs";
1
+ import { t as SSOPlugin } from "./index-CvpS40sl.mjs";
2
2
 
3
3
  //#region src/client.d.ts
4
4
  interface SSOClientOptions {
@@ -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-ZWFEs7WQ.mjs";
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.8",
4
+ "version": "1.5.0-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.8"
70
+ "better-auth": "1.5.0-beta.1"
71
71
  },
72
72
  "peerDependencies": {
73
- "better-auth": "1.4.8"
73
+ "better-auth": "1.5.0-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 as any, {
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: EndpointContext,
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: EndpointContext,
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
+ });
@@ -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
  /**