@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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @better-auth/sso@1.4.8-beta.7 build /home/runner/work/better-auth/better-auth/packages/sso
2
+ > @better-auth/sso@1.4.9 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.65 kB │ gzip: 18.13 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-DNWhGQW-.d.mts 42.86 kB │ gzip: 8.79 kB
15
- ℹ 5 files, total: 137.63 kB
16
- ✔ Build complete in 16370ms
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 22963ms
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as SSOPlugin } from "./index-DNWhGQW-.mjs";
1
+ import { t as SSOPlugin } from "./index-CvpS40sl.mjs";
2
2
 
3
3
  //#region src/client.d.ts
4
4
  interface SSOClientOptions {
@@ -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
- } & SSOProvider<O> : SSOProvider<O>>;
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-DNWhGQW-.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
@@ -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
- return ctx.json({
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: Buffer.from(SAMLResponse, "base64").toString("utf-8")
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: Buffer.from(SAMLResponse, "base64").toString("utf-8")
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(Buffer.from(SAMLResponse, "base64").toString("utf-8"));
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.8-beta.7",
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.8-beta.7"
70
+ "better-auth": "1.4.9"
70
71
  },
71
72
  "peerDependencies": {
72
- "better-auth": "1.4.8-beta.7"
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
- } & SSOProvider<O>
844
- : SSOProvider<O>;
860
+ }
861
+ : SSOProviderResponse;
845
862
 
846
- return ctx.json({
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
- } as unknown as SSOProviderReturn);
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: Buffer.from(SAMLResponse, "base64").toString(
1811
- "utf-8",
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: Buffer.from(SAMLResponse, "base64").toString(
2241
- "utf-8",
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 = Buffer.from(SAMLResponse, "base64").toString(
2338
- "utf-8",
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
+ });
@@ -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";