samlesa 3.4.2 → 3.5.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.
@@ -10,6 +10,7 @@ import { namespace } from './urn.js';
10
10
  import postBinding from './binding-post.js';
11
11
  import redirectBinding from './binding-redirect.js';
12
12
  import simpleSignBinding from './binding-simplesign.js';
13
+ import artifactBinding from './binding-artifact.js';
13
14
  import { flow } from './flow.js';
14
15
  /**
15
16
  * Identity provider can be configured using either metadata importing or idpSetting
@@ -36,7 +37,7 @@ export class IdentityProvider extends Entity {
36
37
  * @param params
37
38
  */
38
39
  async createLoginResponse(params) {
39
- const bindType = params?.binding ?? 'post';
40
+ const bindType = (params?.binding ?? 'post');
40
41
  const { sp, requestInfo = {}, user = {}, customTagReplacement, encryptThenSign = false, relayState = '', AttributeStatement = [], idpInit = false, } = params;
41
42
  const protocol = namespace.binding[bindType];
42
43
  // can support post, redirect and post simple sign bindings for login response
@@ -66,6 +67,20 @@ export class IdentityProvider extends Entity {
66
67
  idp: this,
67
68
  sp,
68
69
  }, user, relayState, customTagReplacement, AttributeStatement);
70
+ case namespace.binding.artifact:
71
+ context = await artifactBinding.soapLoginResponse({
72
+ requestInfo,
73
+ entity: {
74
+ idp: this,
75
+ sp,
76
+ },
77
+ user,
78
+ customTagReplacement,
79
+ encryptThenSign,
80
+ AttributeStatement,
81
+ idpInit,
82
+ });
83
+ break;
69
84
  default:
70
85
  throw new Error('ERR_CREATE_RESPONSE_UNDEFINED_BINDING');
71
86
  }
@@ -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
@@ -117,10 +117,27 @@ export const loginRequestFields = [
117
117
  { key: 'signature', localPath: ['AuthnRequest', 'Signature'], attributes: [], context: true }
118
118
  ];
119
119
  export const artifactResolveFields = [
120
- { key: 'request', localPath: ['ArtifactResolve'], attributes: ['ID', 'IssueInstant', 'Version'] },
121
- { key: 'issuer', localPath: ['ArtifactResolve', 'Issuer'], attributes: [] },
122
- { key: 'Artifact', localPath: ['ArtifactResolve', 'Artifact'], attributes: [] },
123
- { key: 'signature', localPath: ['ArtifactResolve', 'Signature'], attributes: [], context: true },
120
+ {
121
+ key: 'request',
122
+ localPath: ['Envelope', 'Body', 'ArtifactResolve'],
123
+ attributes: ['ID', 'IssueInstant', 'Version', 'Destination']
124
+ },
125
+ {
126
+ key: 'issuer',
127
+ localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Issuer'],
128
+ attributes: []
129
+ },
130
+ {
131
+ key: 'artifact',
132
+ localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Artifact'],
133
+ attributes: []
134
+ },
135
+ {
136
+ key: 'signature',
137
+ localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Signature'],
138
+ attributes: [],
139
+ context: true
140
+ },
124
141
  ];
125
142
  export const artifactResponseFields = [
126
143
  { key: 'request', localPath: ['Envelope', 'Body', 'ArtifactResolve'], attributes: ['ID', 'IssueInstant', 'Version'] },
@@ -1487,6 +1504,9 @@ export function extractSp(context) {
1487
1504
  export function extractAuthRequest(context) {
1488
1505
  return extract(context, loginRequestFields);
1489
1506
  }
1490
- export function extractResponse(context, ass) {
1507
+ export function extractResponse(context) {
1491
1508
  return extractSpToll(context, loginResponseFieldsFullList);
1492
1509
  }
1510
+ export function extractArtifactResolve(context) {
1511
+ return extract(context, artifactResolveFields);
1512
+ }
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,8 @@ import { fileURLToPath } from 'node:url';
5
5
  import { DOMParser } from '@xmldom/xmldom';
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
8
- let normal = [
8
+ // 定义各个场景所需的 schema 文件列表(保持不变)
9
+ const normalSchemas = [
9
10
  'saml-schema-protocol-2.0.xsd',
10
11
  'saml-schema-assertion-2.0.xsd',
11
12
  'xmldsig-core-schema.xsd',
@@ -15,42 +16,38 @@ let normal = [
15
16
  'saml-schema-ecp-2.0.xsd',
16
17
  'saml-schema-dce-2.0.xsd'
17
18
  ];
18
- let soapSchema = [
19
+ const soapSchemas = [
19
20
  'soap-envelope.xsd',
20
21
  'xml.xsd',
21
- // 2. SOAP核心模式(所有SOAP消息的基础)
22
- // 3. XML签名模式(SAML签名的前置依赖)
23
22
  'xmldsig-core-schema.xsd',
24
- // 4. XML加密模式(SAML断言加密的前置依赖)
25
23
  'xenc-schema.xsd',
26
24
  'xenc-schema-11.xsd',
27
- // 5. SAML核心模式(最基础的SAML组件)
28
- 'saml-schema-assertion-2.0.xsd', // 断言定义
29
- // 6. SAML协议模式(依赖断言模式)
25
+ 'saml-schema-assertion-2.0.xsd',
30
26
  'saml-schema-protocol-2.0.xsd',
31
- // 7. SAML扩展模式(依赖核心模式)
32
- 'saml-schema-metadata-2.0.xsd', // 元数据
33
- 'saml-schema-ecp-2.0.xsd', // ECP扩展
34
- 'saml-schema-dce-2.0.xsd' // DCE扩展
27
+ 'saml-schema-metadata-2.0.xsd',
28
+ 'saml-schema-ecp-2.0.xsd',
29
+ 'saml-schema-dce-2.0.xsd'
35
30
  ];
36
- let meta = [
37
- 'saml-schema-metadata-2.0.xsd', // 元数据
31
+ const metadataSchemas = [
32
+ 'saml-schema-metadata-2.0.xsd',
38
33
  'xml.xsd',
39
34
  'saml-schema-assertion-2.0.xsd',
40
35
  'xmldsig-core-schema.xsd',
41
36
  'xenc-schema.xsd',
42
- 'xenc-schema-11.xsd',
37
+ 'xenc-schema-11.xsd'
43
38
  ];
44
- let schemas = normal;
39
+ /**
40
+ * 检测 XML 字符串中是否存在 XXE 攻击指示器
41
+ * @param samlString 待检测的 XML 字符串
42
+ * @returns 如果存在可疑模式则返回匹配详情,否则返回 null
43
+ */
45
44
  function detectXXEIndicators(samlString) {
46
45
  const xxePatterns = [
47
- /<!DOCTYPE\s[^>]*>/i,
48
- /<!ENTITY\s+[^\s>]+\s+(?:SYSTEM|PUBLIC)\s+['"][^>]*>/i,
49
- /&[a-zA-Z0-9._-]+;/g,
50
- /SYSTEM\s*=/i,
51
- /PUBLIC\s*=/i,
52
- /file:\/\//,
53
- /\.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 引用
54
51
  ];
55
52
  const matches = {};
56
53
  xxePatterns.forEach((pattern, index) => {
@@ -64,94 +61,112 @@ function detectXXEIndicators(samlString) {
64
61
  });
65
62
  return Object.keys(matches).length > 0 ? matches : null;
66
63
  }
64
+ /**
65
+ * 加载指定的 schema 文件内容
66
+ * @param schemaNames 文件名数组
67
+ * @returns 包含 fileName 和 contents 的对象数组
68
+ */
69
+ async function loadSchemas(schemaNames) {
70
+ const schemaPath = path.resolve(__dirname, 'schema');
71
+ return Promise.all(schemaNames.map(async (file) => ({
72
+ fileName: file,
73
+ contents: await fs.promises.readFile(`${schemaPath}/${file}`, 'utf-8')
74
+ })));
75
+ }
76
+ /**
77
+ * 验证 SAML 消息(普通或 SOAP)
78
+ * @param xml XML 字符串
79
+ * @param isSoap 是否为 SOAP 消息,默认 false
80
+ * @returns true 表示验证通过,否则抛出错误
81
+ * @throws 当检测到 XXE 或验证失败时抛出错误
82
+ */
67
83
  export const validate = async (xml, isSoap = false) => {
84
+ // 检测 XXE 攻击
68
85
  const indicators = detectXXEIndicators(xml);
69
86
  if (indicators) {
70
87
  throw new Error('ERR_EXCEPTION_VALIDATE_XML');
71
88
  }
72
- schemas = isSoap ? soapSchema : normal;
73
- const schemaPath = path.resolve(__dirname, 'schema');
74
- const [xmlParse, ...preload] = await Promise.all(schemas.map(async (file) => ({
75
- fileName: file,
76
- contents: await fs.promises.readFile(`${schemaPath}/${file}`, 'utf-8')
77
- })));
89
+ // 根据类型选择对应的 schema 列表(避免全局变量并发问题)
90
+ const schemaList = isSoap ? soapSchemas : normalSchemas;
91
+ const schemas = await loadSchemas(schemaList);
78
92
  try {
79
93
  const validationResult = await validateXML({
80
- xml: [
81
- {
82
- fileName: 'content.xml',
83
- contents: xml,
84
- },
85
- ],
94
+ xml: [{ fileName: 'content.xml', contents: xml }],
86
95
  extension: 'schema',
87
- schema: [xmlParse],
88
- preload: [xmlParse, ...preload],
96
+ schema: [schemas[0]], // 第一个 schema 作为主入口
97
+ preload: [schemas[0], ...schemas.slice(1)], // 其余作为预加载
89
98
  });
90
99
  if (validationResult.valid) {
91
100
  return true;
92
101
  }
102
+ // 验证失败,抛出错误对象
93
103
  throw validationResult.errors;
94
104
  }
95
105
  catch (error) {
96
- throw new Error('ERR_EXCEPTION_VALIDATE_XML');
106
+ // 保留原始错误信息
107
+ throw error;
97
108
  }
98
109
  };
110
+ /**
111
+ * 验证 SAML 元数据,并可选择解析元数据类型
112
+ * @param xml XML 字符串
113
+ * @param isParse 是否解析并返回元数据类型,默认 false
114
+ * @returns 验证通过时:若 isParse 为 true 返回 { isValid: true, metadataType: string },否则返回 true;
115
+ * 验证失败时返回 Error 对象(保持原行为)
116
+ */
99
117
  export const validateMetadata = async (xml, isParse = false) => {
118
+ // 检测 XXE 攻击
100
119
  const indicators = detectXXEIndicators(xml);
101
120
  if (indicators) {
102
121
  throw new Error('ERR_EXCEPTION_VALIDATE_XML');
103
122
  }
104
- schemas = meta;
105
- const schemaPath = path.resolve(__dirname, 'schema');
106
- const [xmlParse, ...preload] = await Promise.all(schemas.map(async (file) => ({
107
- fileName: file,
108
- contents: await fs.promises.readFile(`${schemaPath}/${file}`, 'utf-8')
109
- })));
123
+ const schemas = await loadSchemas(metadataSchemas);
110
124
  try {
125
+ // @ts-ignore
111
126
  const validationResult = await validateXML({
112
- xml: [
113
- {
114
- fileName: 'content.xml',
115
- contents: xml,
116
- },
117
- ],
127
+ xml: [{ fileName: 'content.xml', contents: xml }],
118
128
  extension: 'schema',
119
- schema: [xmlParse],
120
- preload: [xmlParse, ...preload],
129
+ schema: [schemas[0]],
130
+ preload: [schemas[0], ...schemas.slice(1)],
121
131
  });
122
132
  if (validationResult.valid) {
123
133
  if (isParse) {
124
- // 解析 XML 为 DOM 对象
134
+ // 解析 XML 并确定元数据类型
125
135
  const parser = new DOMParser();
126
136
  const xmlDoc = parser.parseFromString(xml, 'text/xml');
127
- // 检查 IdP 和 SP 描述符元素
137
+ // 检查解析错误(防御性编程)
138
+ const parserError = xmlDoc.getElementsByTagName('parsererror');
139
+ if (parserError.length > 0) {
140
+ // 解析失败,视为无效 XML,返回错误对象(与原逻辑一致)
141
+ return new Error('XML parsing failed');
142
+ }
128
143
  const idpDescriptor = xmlDoc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:metadata', 'IDPSSODescriptor');
129
144
  const spDescriptor = xmlDoc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:metadata', 'SPSSODescriptor');
130
- // 判断元数据类型
131
145
  let metadataType;
132
146
  if (idpDescriptor.length > 0 && spDescriptor.length > 0) {
133
- metadataType = 'both'; // 同时包含 IdP 和 SP
147
+ metadataType = 'both';
134
148
  }
135
149
  else if (idpDescriptor.length > 0) {
136
- metadataType = 'IdP'; // 身份提供者
150
+ metadataType = 'IdP';
137
151
  }
138
152
  else if (spDescriptor.length > 0) {
139
- metadataType = 'SP'; // 服务提供者
153
+ metadataType = 'SP';
140
154
  }
141
155
  else {
142
- metadataType = 'unknown'; // 无法确定
156
+ metadataType = 'unknown';
143
157
  }
144
- // 返回验证结果和元数据类型
145
158
  return {
146
159
  isValid: true,
147
- metadataType: metadataType
160
+ metadataType
148
161
  };
149
162
  }
150
163
  return true;
151
164
  }
152
- throw validationResult.errors;
165
+ // 验证失败,返回错误对象(保持原行为)
166
+ return validationResult.errors;
153
167
  }
154
168
  catch (error) {
155
- return error;
169
+ // 捕获其他异常(如文件读取失败)并返回错误对象
170
+ return error instanceof Error ? error : new Error(String(error));
156
171
  }
157
172
  };
package/build/src/urn.js CHANGED
@@ -10,6 +10,16 @@ export var BindingNamespace;
10
10
  BindingNamespace["SimpleSign"] = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign";
11
11
  BindingNamespace["Artifact"] = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact";
12
12
  })(BindingNamespace || (BindingNamespace = {}));
13
+ export const NamespaceBindingMap = {
14
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect': 'redirect',
15
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST': 'post',
16
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign': 'simplesign',
17
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact': 'artifact'
18
+ };
19
+ // 可选:添加反向查找函数
20
+ function getBindingName(uri) {
21
+ return NamespaceBindingMap[uri];
22
+ }
13
23
  export var MessageSignatureOrder;
14
24
  (function (MessageSignatureOrder) {
15
25
  MessageSignatureOrder["STE"] = "sign-then-encrypt";
@@ -51,6 +61,12 @@ const namespace = {
51
61
  artifact: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact',
52
62
  soap: 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP',
53
63
  },
64
+ bindMap: {
65
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect': 'redirect',
66
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST': 'post',
67
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign': 'simplesign',
68
+ 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact': 'artifact'
69
+ },
54
70
  names: {
55
71
  protocol: 'urn:oasis:names:tc:SAML:2.0:protocol',
56
72
  assertion: 'urn:oasis:names:tc:SAML:2.0:assertion',
@@ -173,22 +189,31 @@ const messageConfigurations = {
173
189
  const algorithms = {
174
190
  // 1. 签名算法定义 (SignatureMethod)
175
191
  signature: {
176
- // ❌ 原文错误修正:ECDSA 不能用 rsa-sha256 的 URI
177
- ECDSA_SHA256: 'http://www.w3.org/2007/05/xmldsig-more#ecdsa-sha256',
178
- ECDSA_SHA384: 'http://www.w3.org/2007/05/xmldsig-more#ecdsa-sha384',
179
- ECDSA_SHA512: 'http://www.w3.org/2007/05/xmldsig-more#ecdsa-sha512',
180
- DSA_SHA1: 'http://www.w3.org/2000/09/xmldsig#dsa-sha1',
181
- 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 算法(推荐)
182
196
  RSA_SHA224: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha224',
183
- 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', // 推荐
184
198
  RSA_SHA384: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384',
185
199
  RSA_SHA512: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512',
186
- // 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 填充(更安全)
187
205
  RSA_PSS_SHA256: 'http://www.w3.org/2007/05/xmldsig-more#rsa-pss-sha256',
188
- // EdDSA (Ed25519)
189
- 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', // ⭐ 推荐
190
208
  EDDSA_ED488: 'http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448'
191
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
+ ],
192
217
  // 2. 摘要算法定义 (DigestMethod)
193
218
  // 注意:这里直接使用标准推荐的 URI,SHA-2xx 系列推荐使用 xmlenc 命名空间
194
219
  digest: {
@@ -306,4 +331,77 @@ const elementsOrder = {
306
331
  onelogin: ['KeyDescriptor', 'NameIDFormat', 'ArtifactResolutionService', 'SingleLogoutService', 'AssertionConsumerService', 'AttributeConsumingService'],
307
332
  shibboleth: ['KeyDescriptor', 'ArtifactResolutionService', 'SingleLogoutService', 'NameIDFormat', 'AssertionConsumerService', 'AttributeConsumingService',],
308
333
  };
309
- export { namespace, tags, algorithms, wording, elementsOrder, messageConfigurations };
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 };
@@ -312,6 +312,75 @@ export function castArrayOpt(a) {
312
312
  export function notEmpty(value) {
313
313
  return value !== null && value !== undefined;
314
314
  }
315
+ /**
316
+ * @desc 验证 RelayState 是否符合 SAML 2.0 规范
317
+ * @param {string} relayState - RelayState 值
318
+ * @returns {{ valid: boolean; error?: string }} 验证结果
319
+ */
320
+ export function validateRelayState(relayState) {
321
+ // RelayState 是可选的
322
+ if (!relayState || relayState.length === 0) {
323
+ return { valid: true };
324
+ }
325
+ // 验证长度(SAML 规范限制 80 字节)
326
+ if (relayState.length > 80) {
327
+ return {
328
+ valid: false,
329
+ error: 'RelayState exceeds 80 bytes'
330
+ };
331
+ }
332
+ // 验证是否为合法 URL(如果是 URL)
333
+ if (relayState.startsWith('http://') || relayState.startsWith('https://')) {
334
+ try {
335
+ new URL(relayState);
336
+ }
337
+ catch {
338
+ return {
339
+ valid: false,
340
+ error: 'RelayState is not a valid URL'
341
+ };
342
+ }
343
+ }
344
+ return { valid: true };
345
+ }
346
+ /**
347
+ * @desc 敏感信息键名列表(用于日志脱敏)
348
+ */
349
+ const sensitiveKeys = [
350
+ 'privateKey',
351
+ 'privateKeyPass',
352
+ 'encPrivateKey',
353
+ 'encPrivateKeyPass',
354
+ 'password',
355
+ 'secret',
356
+ 'signingCert',
357
+ 'encryptCert'
358
+ ];
359
+ /**
360
+ * @desc 日志脱敏函数,过滤敏感信息
361
+ * @param {any} data - 需要脱敏的数据
362
+ * @returns {any} 脱敏后的数据
363
+ */
364
+ export function sanitizeLog(data) {
365
+ if (typeof data !== 'object' || data === null) {
366
+ return data;
367
+ }
368
+ const sanitized = Array.isArray(data) ? [] : {};
369
+ for (const [key, value] of Object.entries(data)) {
370
+ // 检查是否为敏感键名
371
+ if (sensitiveKeys.some(k => k.toLowerCase() === key.toLowerCase())) {
372
+ sanitized[key] = '***REDACTED***';
373
+ }
374
+ else if (typeof value === 'object' && value !== null) {
375
+ // 递归处理嵌套对象
376
+ sanitized[key] = sanitizeLog(value);
377
+ }
378
+ else {
379
+ sanitized[key] = value;
380
+ }
381
+ }
382
+ return sanitized;
383
+ }
315
384
  const utility = {
316
385
  isString,
317
386
  base64Encode,
@@ -327,5 +396,7 @@ const utility = {
327
396
  readPrivateKey,
328
397
  convertToString,
329
398
  isNonEmptyArray,
399
+ validateRelayState,
400
+ sanitizeLog,
330
401
  };
331
402
  export default utility;