samlesa 3.4.3 → 4.0.0

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.
@@ -4,12 +4,11 @@
4
4
  * @desc Declares the actions taken by service provider
5
5
  */
6
6
  import Entity from './entity.js';
7
- import Artifact from './binding-artifact.js';
7
+ import artifactBinding from './binding-artifact.js';
8
8
  import { namespace } from './urn.js';
9
9
  import redirectBinding from './binding-redirect.js';
10
10
  import postBinding from './binding-post.js';
11
11
  import simpleSignBinding from './binding-simplesign.js';
12
- import artifactSignBinding from './binding-artifact.js';
13
12
  import { flow } from './flow.js';
14
13
  /*
15
14
  * @desc interface function
@@ -19,8 +18,7 @@ export default function (props) {
19
18
  }
20
19
  /**
21
20
  * @desc Service provider can be configured using either metadata importing or spSetting
22
- * @param {object} spSettingimport { FlowResult } from '../types/src/flow.d';
23
-
21
+ * @param {object} spSetting
24
22
  */
25
23
  export class ServiceProvider extends Entity {
26
24
  /**
@@ -61,8 +59,13 @@ export class ServiceProvider extends Entity {
61
59
  // Object context = {id, context, signature, sigAlg}
62
60
  context = simpleSignBinding.base64LoginRequest({ idp, sp: this }, customTagReplacement);
63
61
  break;
62
+ case nsBinding.artifact:
63
+ context = artifactBinding.soapLoginRequest("/*[local-name(.)='AuthnRequest']", {
64
+ idp,
65
+ sp: this
66
+ }, customTagReplacement);
67
+ break;
64
68
  default:
65
- // Will support artifact in the next release
66
69
  throw new Error('ERR_SP_LOGIN_REQUEST_UNDEFINED_BINDING');
67
70
  }
68
71
  return {
@@ -73,13 +76,7 @@ export class ServiceProvider extends Entity {
73
76
  };
74
77
  }
75
78
  async createLoginSoapRequest(idp, binding = 'artifact', config) {
76
- const nsBinding = namespace.binding;
77
- const protocol = nsBinding[binding];
78
- if (this.entityMeta.isAuthnRequestSigned() !== idp.entityMeta.isWantAuthnRequestsSigned()) {
79
- throw new Error('ERR_METADATA_CONFLICT_REQUEST_SIGNED_FLAG');
80
- }
81
- let context = null;
82
- context = await artifactSignBinding.soapLoginRequest("/*[local-name(.)='AuthnRequest']", {
79
+ const context = await artifactBinding.soapLoginRequest("/*[local-name(.)='AuthnRequest']", {
83
80
  idp,
84
81
  sp: this,
85
82
  inResponse: config?.inResponseTo,
@@ -106,22 +103,27 @@ export class ServiceProvider extends Entity {
106
103
  });
107
104
  }
108
105
  /**
109
- * @desc request SamlResponse by Arc id
106
+ * @desc Parse and validate Artifact Resolve request
110
107
  * @param {IdentityProvider} idp object of identity provider
111
- * @param {string} binding protocol binding
112
- * @param {request} req request
108
+ * @param {string} xml SOAP request XML string
113
109
  */
114
110
  parseLoginRequestResolve(idp, xml) {
115
111
  const self = this;
116
- return Artifact.parseLoginRequestResolve({
112
+ return artifactBinding.parseLoginRequestResolve({
117
113
  idp: idp,
118
114
  sp: self,
119
115
  xml: xml
120
116
  });
121
117
  }
118
+ /**
119
+ * @desc Resolve SAML Response by Artifact ID
120
+ * @param {IdentityProvider} idp object of identity provider
121
+ * @param {string} art Artifact string
122
+ * @param {request} req request
123
+ */
122
124
  parseLoginResponseResolve(idp, art, request) {
123
125
  const self = this;
124
- return Artifact.parseLoginResponseResolve({
126
+ return artifactBinding.parseLoginResponseResolve({
125
127
  idp: idp,
126
128
  sp: self,
127
129
  art: art
package/build/src/flow.js CHANGED
@@ -225,7 +225,7 @@ async function postFlow(options) {
225
225
  if (parserType === 'SAMLResponse'
226
226
  && extractedProperties.conditions
227
227
  && !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, self.entitySetting.clockDrifts)) {
228
- return Promise.reject('ERR_CONDITION_SESSION');
228
+ return Promise.reject('ERR_CONDITION_UNCONFIRMED');
229
229
  }
230
230
  // invalid subjectConfirmation time
231
231
  // invalid time
@@ -416,13 +416,6 @@ async function postArtifactFlow(options) {
416
416
  //There is no validation of the response here. The upper-layer application
417
417
  // should verify the result by itself to see if the destination is equal to the SP acs and
418
418
  // whether the response.id is used to prevent replay attacks.
419
- let destination = extractedProperties?.response?.destination;
420
- let isExit = self?.entityMeta?.meta?.assertionConsumerService?.filter((item) => {
421
- return item?.location === destination;
422
- });
423
- if (isExit?.length === 0) {
424
- return Promise.reject('ERR_Destination_URL');
425
- }
426
419
  if (parserType === 'SAMLResponse') {
427
420
  let destination = extractedProperties?.response?.destination;
428
421
  let isExit = self?.entityMeta?.meta?.assertionConsumerService?.filter((item) => {
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { X509Certificate } from 'node:crypto';
7
7
  import xml from 'xml';
8
- import utility, { flattenDeep, inflateString, isString } from './utility.js';
8
+ import utility, { inflateString, isString, normalizeCertificates } from './utility.js';
9
9
  ;
10
10
  import * as crypto from 'node:crypto';
11
11
  import { algorithms, namespace, wording } from './urn.js';
@@ -524,13 +524,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
524
524
  else if (opts.metadata) {
525
525
  const certificateNode = select(".//*[local-name() = 'X509Certificate']", signatureNode);
526
526
  let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
527
- if (Array.isArray(metadataCert)) {
528
- metadataCert = flattenDeep(metadataCert);
529
- }
530
- else if (typeof metadataCert === 'string') {
531
- metadataCert = [metadataCert];
532
- }
533
- metadataCert = metadataCert.map(utility.normalizeCerString);
527
+ metadataCert = normalizeCertificates(metadataCert);
534
528
  if (certificateNode.length === 0 && metadataCert.length === 0) {
535
529
  throw new Error('NO_SELECTED_CERTIFICATE');
536
530
  }
@@ -580,14 +574,12 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
580
574
  }
581
575
  else if (opts.metadata) {
582
576
  const certificateNode = select(".//*[local-name() = 'X509Certificate']", signatureNode);
577
+ console.log(opts.metadata.getX509Certificate);
578
+ console.log(certUse.signing);
579
+ console.log("执行情况");
580
+ console.log(opts);
583
581
  let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
584
- if (Array.isArray(metadataCert)) {
585
- metadataCert = flattenDeep(metadataCert);
586
- }
587
- else if (typeof metadataCert === 'string') {
588
- metadataCert = [metadataCert];
589
- }
590
- metadataCert = metadataCert.map(utility.normalizeCerString);
582
+ metadataCert = normalizeCertificates(metadataCert);
591
583
  if (certificateNode.length === 0 && metadataCert.length === 0) {
592
584
  throw new Error('NO_SELECTED_CERTIFICATE');
593
585
  }
@@ -627,13 +619,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
627
619
  else if (opts.metadata) {
628
620
  const certificateNode = select(".//*[local-name() = 'X509Certificate']", signatureNode);
629
621
  let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
630
- if (Array.isArray(metadataCert)) {
631
- metadataCert = flattenDeep(metadataCert);
632
- }
633
- else if (typeof metadataCert === 'string') {
634
- metadataCert = [metadataCert];
635
- }
636
- metadataCert = metadataCert.map(utility.normalizeCerString);
622
+ metadataCert = normalizeCertificates(metadataCert);
637
623
  if (certificateNode.length === 0 && metadataCert.length === 0) {
638
624
  throw new Error('NO_SELECTED_CERTIFICATE');
639
625
  }
@@ -791,13 +777,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
791
777
  const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
792
778
  // 证书处理逻辑
793
779
  let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
794
- if (Array.isArray(metadataCert)) {
795
- metadataCert = flattenDeep(metadataCert);
796
- }
797
- else if (typeof metadataCert === 'string') {
798
- metadataCert = [metadataCert];
799
- }
800
- metadataCert = metadataCert.map(utility.normalizeCerString);
780
+ metadataCert = normalizeCertificates(metadataCert);
801
781
  // 没有证书的情况
802
782
  if (certificateNode.length === 0 && metadataCert.length === 0) {
803
783
  throw new Error('NO_SELECTED_CERTIFICATE');
@@ -1070,13 +1050,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
1070
1050
  else if (opts.metadata) {
1071
1051
  const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
1072
1052
  let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
1073
- if (Array.isArray(metadataCert)) {
1074
- metadataCert = flattenDeep(metadataCert);
1075
- }
1076
- else if (typeof metadataCert === 'string') {
1077
- metadataCert = [metadataCert];
1078
- }
1079
- metadataCert = metadataCert.map(utility.normalizeCerString);
1053
+ metadataCert = normalizeCertificates(metadataCert);
1080
1054
  if (certificateNode.length === 0 && metadataCert.length === 0) {
1081
1055
  throw new Error('NO_SELECTED_CERTIFICATE');
1082
1056
  }
@@ -43,13 +43,11 @@ const metadataSchemas = [
43
43
  */
44
44
  function detectXXEIndicators(samlString) {
45
45
  const xxePatterns = [
46
- /<!DOCTYPE\s[^>]*>/i,
47
- /<!ENTITY\s+[^\s>]+\s+(?:SYSTEM|PUBLIC)\s+['"][^>]*>/i,
48
- /&[a-zA-Z0-9._-]+;/g,
49
- /SYSTEM\s*=/i,
50
- /PUBLIC\s*=/i,
51
- /file:\/\//,
52
- /\.dtd['"]?/
46
+ /<!DOCTYPE\s[^>]*>/i, // DOCTYPE 声明
47
+ /<!ENTITY\s+[^\s>]+\s+(?:SYSTEM|PUBLIC)\s+['"][^>]*>/i, // 外部实体声明
48
+ /SYSTEM\s*['"]\s*file:\/\//i, // file:// 协议的系统引用
49
+ /SYSTEM\s*['"]\s*\.\.\/.*\.dtd['"]?/i, // 相对路径的 DTD 引用
50
+ /PUBLIC\s*['"][^'"]*['"]\s*['"][^'"]*\.dtd['"]?/i // 公共 DTD 引用
53
51
  ];
54
52
  const matches = {};
55
53
  xxePatterns.forEach((pattern, index) => {
package/build/src/urn.js CHANGED
@@ -189,22 +189,31 @@ const messageConfigurations = {
189
189
  const algorithms = {
190
190
  // 1. 签名算法定义 (SignatureMethod)
191
191
  signature: {
192
- // ❌ 原文错误修正:ECDSA 不能用 rsa-sha256 的 URI
193
- ECDSA_SHA256: 'http://www.w3.org/2007/05/xmldsig-more#ecdsa-sha256',
194
- ECDSA_SHA384: 'http://www.w3.org/2007/05/xmldsig-more#ecdsa-sha384',
195
- ECDSA_SHA512: 'http://www.w3.org/2007/05/xmldsig-more#ecdsa-sha512',
196
- DSA_SHA1: 'http://www.w3.org/2000/09/xmldsig#dsa-sha1',
197
- RSA_SHA1: 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
192
+ // ❌ 不安全的算法(已废弃)
193
+ RSA_SHA1: 'http://www.w3.org/2000/09/xmldsig#rsa-sha1', // ⚠️ 已废弃,不推荐使用
194
+ DSA_SHA1: 'http://www.w3.org/2000/09/xmldsig#dsa-sha1', // ⚠️ 已废弃,不推荐使用
195
+ // ✅ 安全的 RSA 算法(推荐)
198
196
  RSA_SHA224: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha224',
199
- RSA_SHA256: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', // 推荐
197
+ RSA_SHA256: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', // 推荐
200
198
  RSA_SHA384: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384',
201
199
  RSA_SHA512: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512',
202
- // XML Signature 1.1 PSS 填充 (更安全)
200
+ // ECDSA 算法(推荐)
201
+ ECDSA_SHA256: 'http://www.w3.org/2007/05/xmldsig-more#ecdsa-sha256', // ⭐ 推荐
202
+ ECDSA_SHA384: 'http://www.w3.org/2007/05/xmldsig-more#ecdsa-sha384',
203
+ ECDSA_SHA512: 'http://www.w3.org/2007/05/xmldsig-more#ecdsa-sha512',
204
+ // ✅ XML Signature 1.1 PSS 填充(更安全)
203
205
  RSA_PSS_SHA256: 'http://www.w3.org/2007/05/xmldsig-more#rsa-pss-sha256',
204
- // EdDSA (Ed25519)
205
- EDDSA_ED25519: 'http://www.w3.org/2007/05/xmldsig-more#eddsa-ed25519',
206
+ // EdDSA (Ed25519/Ed448)
207
+ EDDSA_ED25519: 'http://www.w3.org/2007/05/xmldsig-more#eddsa-ed25519', // ⭐ 推荐
206
208
  EDDSA_ED488: 'http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448'
207
209
  },
210
+ // 不安全的算法列表(用于验证和阻止)
211
+ unsafeAlgorithms: [
212
+ 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
213
+ 'http://www.w3.org/2000/09/xmldsig#dsa-sha1',
214
+ 'http://www.w3.org/2000/09/xmldsig#hmac-sha1',
215
+ 'http://www.w3.org/2000/09/xmldsig#sha1',
216
+ ],
208
217
  // 2. 摘要算法定义 (DigestMethod)
209
218
  // 注意:这里直接使用标准推荐的 URI,SHA-2xx 系列推荐使用 xmlenc 命名空间
210
219
  digest: {
@@ -322,4 +331,77 @@ const elementsOrder = {
322
331
  onelogin: ['KeyDescriptor', 'NameIDFormat', 'ArtifactResolutionService', 'SingleLogoutService', 'AssertionConsumerService', 'AttributeConsumingService'],
323
332
  shibboleth: ['KeyDescriptor', 'ArtifactResolutionService', 'SingleLogoutService', 'NameIDFormat', 'AssertionConsumerService', 'AttributeConsumingService',],
324
333
  };
325
- export { namespace, tags, algorithms, wording, elementsOrder, messageConfigurations, getBindingName };
334
+ /**
335
+ * 默认安全配置
336
+ */
337
+ const defaultSecurityOptions = {
338
+ allowSHA1: false,
339
+ allowRSA15: false,
340
+ allowTripleDES: false,
341
+ };
342
+ /**
343
+ * 当前安全配置
344
+ */
345
+ let currentSecurityOptions = { ...defaultSecurityOptions };
346
+ /**
347
+ * 设置安全配置
348
+ * @param options 安全配置选项
349
+ */
350
+ function setSecurityOptions(options) {
351
+ currentSecurityOptions = { ...currentSecurityOptions, ...options };
352
+ }
353
+ /**
354
+ * 获取当前安全配置
355
+ * @returns 安全配置对象
356
+ */
357
+ function getSecurityOptions() {
358
+ return currentSecurityOptions;
359
+ }
360
+ /**
361
+ * 重置为默认安全配置
362
+ */
363
+ function resetSecurityOptions() {
364
+ currentSecurityOptions = { ...defaultSecurityOptions };
365
+ }
366
+ /**
367
+ * 验证算法是否安全
368
+ * @param algorithm 算法 URI
369
+ * @returns 验证结果
370
+ */
371
+ function validateAlgorithm(algorithm) {
372
+ // 检查 SHA-1
373
+ if (!currentSecurityOptions.allowSHA1 && algorithm.toLowerCase().includes('sha1')) {
374
+ return {
375
+ valid: false,
376
+ reason: 'SHA-1 algorithm is not allowed. Use SHA-256 or stronger.'
377
+ };
378
+ }
379
+ // 检查 RSA-1_5
380
+ if (!currentSecurityOptions.allowRSA15 && algorithm.includes('rsa-1_5')) {
381
+ return {
382
+ valid: false,
383
+ reason: 'RSA-1_5 key encryption is not allowed. Use RSA-OAEP instead.'
384
+ };
385
+ }
386
+ // 检查 TripleDES
387
+ if (!currentSecurityOptions.allowTripleDES && algorithm.includes('tripledes')) {
388
+ return {
389
+ valid: false,
390
+ reason: 'TripleDES encryption is not allowed. Use AES-GCM instead.'
391
+ };
392
+ }
393
+ return { valid: true };
394
+ }
395
+ /**
396
+ * 检查算法是否为不安全算法
397
+ * @param algorithm 算法 URI
398
+ * @returns 检查结果
399
+ */
400
+ function checkUnsafeAlgorithm(algorithm) {
401
+ const isUnsafe = algorithms.unsafeAlgorithms.some(unsafeAlg => algorithm.toLowerCase().includes(unsafeAlg.toLowerCase().replace('http://www.w3.org/2000/09/xmldsig#', '').replace('#', ''))) || algorithm.toLowerCase().includes('sha1');
402
+ return {
403
+ isUnsafe,
404
+ algorithm: isUnsafe ? algorithm : undefined
405
+ };
406
+ }
407
+ export { namespace, tags, algorithms, wording, elementsOrder, messageConfigurations, getBindingName, defaultSecurityOptions, setSecurityOptions, getSecurityOptions, resetSecurityOptions, validateAlgorithm, checkUnsafeAlgorithm };
@@ -178,11 +178,21 @@ function applyDefault(obj1, obj2) {
178
178
  * @return {string} public key fetched from the certificate
179
179
  */
180
180
  function getPublicKeyPemFromCertificate(x509CertificateString) {
181
- const derBuffer = Buffer.from(x509CertificateString, 'base64');
181
+ // 清理证书字符串:移除 PEM 头尾、换行符、空格等
182
+ const cleanCert = x509CertificateString
183
+ .replace(/-----BEGIN CERTIFICATE-----/g, '')
184
+ .replace(/-----END CERTIFICATE-----/g, '')
185
+ .replace(/\r\n/g, '')
186
+ .replace(/\n/g, '')
187
+ .replace(/\r/g, '')
188
+ .replace(/ /g, '')
189
+ .trim();
190
+ // 将 Base64 字符串转换为 PEM 格式(添加头尾标记)
191
+ const pemCert = `-----BEGIN CERTIFICATE-----\n${cleanCert}\n-----END CERTIFICATE-----`;
182
192
  // 解析 X.509 证书
183
- const cert2 = new X509Certificate(derBuffer);
193
+ const cert2 = new X509Certificate(pemCert);
184
194
  const publicKeyObject = cert2.publicKey;
185
- // 3. 导出为 PEM 格式
195
+ // 导出为 PEM 格式
186
196
  return publicKeyObject.export({
187
197
  type: 'spki', // 使用 Subject Public Key Info 结构
188
198
  format: 'pem' // 输出 PEM 格式
@@ -312,6 +322,192 @@ export function castArrayOpt(a) {
312
322
  export function notEmpty(value) {
313
323
  return value !== null && value !== undefined;
314
324
  }
325
+ /**
326
+ * @desc 验证 RelayState 是否符合 SAML 2.0 规范
327
+ * @param {string} relayState - RelayState 值
328
+ * @returns {{ valid: boolean; error?: string }} 验证结果
329
+ */
330
+ export function validateRelayState(relayState) {
331
+ // RelayState 是可选的
332
+ if (!relayState || relayState.length === 0) {
333
+ return { valid: true };
334
+ }
335
+ // 验证长度(SAML 规范限制 80 字节)
336
+ if (relayState.length > 80) {
337
+ return {
338
+ valid: false,
339
+ error: 'RelayState exceeds 80 bytes'
340
+ };
341
+ }
342
+ // 验证是否为合法 URL(如果是 URL)
343
+ if (relayState.startsWith('http://') || relayState.startsWith('https://')) {
344
+ try {
345
+ new URL(relayState);
346
+ }
347
+ catch {
348
+ return {
349
+ valid: false,
350
+ error: 'RelayState is not a valid URL'
351
+ };
352
+ }
353
+ }
354
+ return { valid: true };
355
+ }
356
+ /**
357
+ * @desc 敏感信息键名列表(用于日志脱敏)
358
+ */
359
+ const sensitiveKeys = [
360
+ 'privateKey',
361
+ 'privateKeyPass',
362
+ 'encPrivateKey',
363
+ 'encPrivateKeyPass',
364
+ 'password',
365
+ 'secret',
366
+ 'signingCert',
367
+ 'encryptCert'
368
+ ];
369
+ /**
370
+ * @desc 验证并标准化证书数组,处理 null、undefined、空字符串、非数组等边界情况
371
+ * @param {any} metadataCert - 证书输入,可能是 string、string[]、null、undefined 或其他类型
372
+ * @returns {string[]} 标准化后的证书字符串数组(纯 Base64 格式,无 PEM 头尾和换行)
373
+ * @throws {Error} 当证书格式无效时抛出错误
374
+ */
375
+ export function normalizeCertificates(metadataCert) {
376
+ // 处理 null 或 undefined
377
+ if (metadataCert === null || metadataCert === undefined) {
378
+ return [];
379
+ }
380
+ let certArray;
381
+ // 转换为数组
382
+ if (Array.isArray(metadataCert)) {
383
+ // 扁平化嵌套数组
384
+ certArray = flattenDeep(metadataCert);
385
+ }
386
+ else if (typeof metadataCert === 'string') {
387
+ // 单个字符串转为数组
388
+ certArray = [metadataCert];
389
+ }
390
+ else {
391
+ // 不支持的类型
392
+ console.warn(`normalizeCertificates: 不支持的证书类型 ${typeof metadataCert},返回空数组`);
393
+ return [];
394
+ }
395
+ // 过滤和清理证书
396
+ const cleanedCerts = certArray
397
+ .filter((cert) => {
398
+ // 过滤 null、undefined、空字符串
399
+ if (cert === null || cert === undefined) {
400
+ return false;
401
+ }
402
+ if (typeof cert !== 'string') {
403
+ console.warn(`normalizeCertificates: 跳过非字符串证书类型 ${typeof cert}`);
404
+ return false;
405
+ }
406
+ const trimmed = cert.trim();
407
+ if (trimmed.length === 0) {
408
+ return false;
409
+ }
410
+ return true;
411
+ })
412
+ .map((cert) => {
413
+ // 清理证书字符串:移除 PEM 头尾、换行符、空格等
414
+ return cert
415
+ .replace(/-----BEGIN CERTIFICATE-----/g, '')
416
+ .replace(/-----END CERTIFICATE-----/g, '')
417
+ .replace(/\r\n/g, '')
418
+ .replace(/\n/g, '')
419
+ .replace(/\r/g, '')
420
+ .replace(/ /g, '')
421
+ .trim();
422
+ })
423
+ .filter((cert) => cert.length > 0); // 再次过滤空字符串
424
+ // 验证证书格式(可选,仅验证 Base64 格式)
425
+ const base64Regex = /^[A-Za-z0-9+/]+=*$/;
426
+ for (const cert of cleanedCerts) {
427
+ if (!base64Regex.test(cert)) {
428
+ throw new Error(`无效的证书格式:证书必须是有效的 Base64 编码,当前值:${cert.substring(0, 50)}...`);
429
+ }
430
+ }
431
+ return cleanedCerts;
432
+ }
433
+ /**
434
+ * @desc 验证证书是否有效(可选,用于更严格的验证)
435
+ * @param {string} certificateBase64 - Base64 编码的证书(不含 PEM 头尾)
436
+ * @returns {{ isValid: boolean; error?: string }} 验证结果
437
+ */
438
+ export function validateCertificate(certificateBase64) {
439
+ try {
440
+ // 清理证书
441
+ const cleanCert = certificateBase64
442
+ .replace(/-----BEGIN CERTIFICATE-----/g, '')
443
+ .replace(/-----END CERTIFICATE-----/g, '')
444
+ .replace(/\r\n/g, '')
445
+ .replace(/\n/g, '')
446
+ .replace(/\r/g, '')
447
+ .replace(/ /g, '')
448
+ .trim();
449
+ // 验证 Base64 格式
450
+ const base64Regex = /^[A-Za-z0-9+/]+=*$/;
451
+ if (!base64Regex.test(cleanCert)) {
452
+ return {
453
+ isValid: false,
454
+ error: '无效的 Base64 编码'
455
+ };
456
+ }
457
+ // 转换为 PEM 格式
458
+ const pemCert = `-----BEGIN CERTIFICATE-----\n${cleanCert}\n-----END CERTIFICATE-----`;
459
+ // 尝试解析证书
460
+ const cert = new X509Certificate(pemCert);
461
+ // 检查有效期
462
+ const now = new Date();
463
+ if (new Date(cert.validFrom) > now || new Date(cert.validTo) < now) {
464
+ return {
465
+ isValid: false,
466
+ error: '证书已过期或尚未生效'
467
+ };
468
+ }
469
+ // 检查公钥类型
470
+ const keyType = cert.publicKey.asymmetricKeyType;
471
+ if (keyType && !['rsa', 'ec'].includes(keyType)) {
472
+ return {
473
+ isValid: false,
474
+ error: '证书使用不支持的公钥类型'
475
+ };
476
+ }
477
+ return { isValid: true };
478
+ }
479
+ catch (error) {
480
+ return {
481
+ isValid: false,
482
+ error: error instanceof Error ? error.message : '未知错误'
483
+ };
484
+ }
485
+ }
486
+ /**
487
+ * @desc 日志脱敏函数,过滤敏感信息
488
+ * @param {any} data - 需要脱敏的数据
489
+ * @returns {any} 脱敏后的数据
490
+ */
491
+ export function sanitizeLog(data) {
492
+ if (typeof data !== 'object' || data === null) {
493
+ return data;
494
+ }
495
+ const sanitized = Array.isArray(data) ? [] : {};
496
+ for (const [key, value] of Object.entries(data)) {
497
+ // 检查是否为敏感键名
498
+ if (sensitiveKeys.some(k => k.toLowerCase() === key.toLowerCase())) {
499
+ sanitized[key] = '***REDACTED***';
500
+ }
501
+ else if (typeof value === 'object' && value !== null) {
502
+ // 递归处理嵌套对象
503
+ sanitized[key] = sanitizeLog(value);
504
+ }
505
+ else {
506
+ sanitized[key] = value;
507
+ }
508
+ }
509
+ return sanitized;
510
+ }
315
511
  const utility = {
316
512
  isString,
317
513
  base64Encode,
@@ -327,5 +523,9 @@ const utility = {
327
523
  readPrivateKey,
328
524
  convertToString,
329
525
  isNonEmptyArray,
526
+ validateRelayState,
527
+ sanitizeLog,
528
+ normalizeCertificates,
529
+ validateCertificate,
330
530
  };
331
531
  export default utility;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "samlesa",
3
- "version": "3.4.3",
3
+ "version": "4.0.0",
4
4
  "description": "High-level API for Single Sign On (SAML 2.0) baseed on samlify ",
5
5
  "main": "build/index.js",
6
6
  "keywords": [
@@ -12,14 +12,23 @@
12
12
  ],
13
13
  "type": "module",
14
14
  "typings": "types/index.d.ts",
15
+ "homepage": "https://saml.veclea.com",
15
16
  "scripts": {
16
17
  "build": "tsc && copyfiles -u 1 src/schema/**/* build/src",
17
- "docs": "docsify serve -o docs",
18
+ "build:fast": "tsc",
19
+ "build:clean": "tsc --build --clean && pnpm run build",
20
+ "docs:dev": "cd docs && npm run docs:dev",
21
+ "docs:build": "cd docs && npm run docs:build",
22
+ "docs:preview": "cd docs && npm run docs:preview",
23
+ "docs:deploy": "vercel --prod",
18
24
  "lint": "tslint -p .",
19
25
  "lint:fix": "tslint -p . --fix",
20
- "test": "vitest",
21
- "test:watch": "vitest --watch",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
22
28
  "test:coverage": "vitest run --coverage",
29
+ "test:fast": "vitest run --pool=forks",
30
+ "test:artifact": "vitest run test/artifact.test.ts",
31
+ "generate-certs": "node scripts/generate-certs.js",
23
32
  "hooks:postinstall": "mklink /J .git\\hooks\\pre-commit .pre-commit.sh || copy .pre-commit.sh .git\\hooks\\pre-commit"
24
33
  },
25
34
  "exports": {
@@ -40,6 +49,10 @@
40
49
  "url": "https://github.com/Veclea/samlify.git",
41
50
  "type": "git"
42
51
  },
52
+ "bugs": {
53
+ "url": "https://github.com/Veclea/samlify/issues"
54
+ },
55
+ "docs": "https://saml.veclea.com",
43
56
  "license": "MIT",
44
57
  "dependencies": {
45
58
  "@xmldom/xmldom": "^0.9.8",