samlesa 3.3.7 → 3.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/extractor.js +1082 -24
- package/package.json +79 -78
- package/types/src/extractor.d.ts +10 -0
- package/types/src/extractor.d.ts.map +1 -1
- package/types/src/libsaml.d.ts.map +1 -1
package/build/src/extractor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { select } from 'xpath';
|
|
2
|
-
import { uniq, last, zipObject, notEmpty } from './utility.js';
|
|
3
|
-
import { getContext } from './api.js';
|
|
2
|
+
import { uniq, last, zipObject, notEmpty } from './utility.js'; // 假设这些工具函数存在
|
|
3
|
+
import { getContext } from './api.js'; // 假设这个API存在
|
|
4
4
|
import camelCase from 'camelcase';
|
|
5
5
|
function buildAbsoluteXPath(paths) {
|
|
6
6
|
if (!paths || paths.length === 0)
|
|
@@ -28,14 +28,85 @@ function buildAttributeXPath(attributes) {
|
|
|
28
28
|
const filters = attributes.map(attribute => `name()='${attribute}'`).join(' or ');
|
|
29
29
|
return `/@*[${filters}]`;
|
|
30
30
|
}
|
|
31
|
-
// ... (其他字段配置如 loginRequestFields 等保持不变,为节省篇幅此处省略,请保留你原有的所有 fields 定义) ...
|
|
32
|
-
// 为了完整性,这里假设你保留了之前所有的 fields 定义 (loginRequestFields, idpMetadataFields 等)
|
|
33
|
-
// 重点修正下方的 spMetadataFields 和 extract 函数
|
|
34
31
|
export const loginRequestFields = [
|
|
35
|
-
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
// --- 1. AuthnRequest 根元素增强 ---
|
|
33
|
+
{
|
|
34
|
+
key: 'request',
|
|
35
|
+
localPath: ['AuthnRequest'],
|
|
36
|
+
attributes: [
|
|
37
|
+
'ID',
|
|
38
|
+
'Version', // [新增] 版本号
|
|
39
|
+
'IssueInstant',
|
|
40
|
+
'Destination',
|
|
41
|
+
'Consent', // [新增] 用户同意状态
|
|
42
|
+
'AssertionConsumerServiceURL',
|
|
43
|
+
'ProtocolBinding',
|
|
44
|
+
'ForceAuthn',
|
|
45
|
+
'IsPassive',
|
|
46
|
+
'AssertionConsumerServiceIndex',
|
|
47
|
+
'AttributeConsumingServiceIndex',
|
|
48
|
+
'ProviderName' // [新增] SP 显示名称
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
// --- 2. Issuer ---
|
|
52
|
+
{ key: 'issuer', localPath: ['AuthnRequest', 'Issuer'], attributes: [] }, // [增强] 提取 Issuer 的属性
|
|
53
|
+
// --- 3. NameIDPolicy 增强 ---
|
|
54
|
+
{
|
|
55
|
+
key: 'nameIDPolicy',
|
|
56
|
+
localPath: ['AuthnRequest', 'NameIDPolicy'],
|
|
57
|
+
attributes: [
|
|
58
|
+
'Format',
|
|
59
|
+
'AllowCreate',
|
|
60
|
+
'SPNameQualifier' // [新增] 关键属性,指定 NameID 的作用域
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
// --- 4. RequestedAuthnContext [全新添加] ---
|
|
64
|
+
// 提取比较策略
|
|
65
|
+
{
|
|
66
|
+
key: 'requestedAuthnContextComparison',
|
|
67
|
+
localPath: ['AuthnRequest', 'RequestedAuthnContext'],
|
|
68
|
+
attributes: ['Comparison']
|
|
69
|
+
},
|
|
70
|
+
// 提取要求的认证类列表 (listMode 因为可能有多个 ClassRef)
|
|
71
|
+
{
|
|
72
|
+
key: 'requestedAuthnContextClasses',
|
|
73
|
+
localPath: ['AuthnRequest', 'RequestedAuthnContext', 'AuthnContextClassRef'],
|
|
74
|
+
attributes: [],
|
|
75
|
+
listMode: true
|
|
76
|
+
},
|
|
77
|
+
// 提取认证声明引用 (较少用,但为了完整性)
|
|
78
|
+
{
|
|
79
|
+
key: 'requestedAuthnContextDeclRefs',
|
|
80
|
+
localPath: ['AuthnRequest', 'RequestedAuthnContext', 'AuthnContextDeclRef'],
|
|
81
|
+
attributes: [],
|
|
82
|
+
listMode: true
|
|
83
|
+
},
|
|
84
|
+
// --- 5. Scoping (用于 IdP 代理场景) [全新添加] ---
|
|
85
|
+
{
|
|
86
|
+
key: 'scoping',
|
|
87
|
+
localPath: ['AuthnRequest', 'Scoping'],
|
|
88
|
+
attributes: ['ProxyCount']
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: 'requesterIDs',
|
|
92
|
+
localPath: ['AuthnRequest', 'Scoping', 'RequesterID'],
|
|
93
|
+
attributes: [],
|
|
94
|
+
listMode: true
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
key: 'idpListEntries',
|
|
98
|
+
localPath: ['AuthnRequest', 'Scoping', 'IDPList', 'IDPEntry'],
|
|
99
|
+
attributes: ['ProviderID', 'Name', 'Loc'],
|
|
100
|
+
listMode: true
|
|
101
|
+
},
|
|
102
|
+
// --- 6. Extensions (捕获自定义扩展) ---
|
|
103
|
+
{
|
|
104
|
+
key: 'extensions',
|
|
105
|
+
localPath: ['AuthnRequest', 'Extensions'],
|
|
106
|
+
attributes: [],
|
|
107
|
+
context: true // 获取整个 XML 片段
|
|
108
|
+
},
|
|
109
|
+
// --- 7. Signature ---
|
|
39
110
|
{ key: 'signature', localPath: ['AuthnRequest', 'Signature'], attributes: [], context: true }
|
|
40
111
|
];
|
|
41
112
|
export const artifactResolveFields = [
|
|
@@ -74,6 +145,201 @@ export const loginResponseFields = assertion => [
|
|
|
74
145
|
{ key: 'oneTimeUse', localPath: ['Assertion', 'Conditions', 'OneTimeUse'], attributes: [], shortcut: assertion },
|
|
75
146
|
{ key: 'status', localPath: ['Response', 'Status', 'StatusCode'], attributes: ['Value'] },
|
|
76
147
|
];
|
|
148
|
+
/*export const loginResponseFieldsFullList: ExtractorFields = [
|
|
149
|
+
// ==========================================================================
|
|
150
|
+
// 1. Response 根元素
|
|
151
|
+
// ==========================================================================
|
|
152
|
+
{
|
|
153
|
+
key: 'response',
|
|
154
|
+
localPath: ['Response'],
|
|
155
|
+
attributes: [
|
|
156
|
+
'ID',
|
|
157
|
+
'Version',
|
|
158
|
+
'IssueInstant',
|
|
159
|
+
'Destination',
|
|
160
|
+
'InResponseTo',
|
|
161
|
+
'Consent',
|
|
162
|
+
'RelayState'
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
// ==========================================================================
|
|
166
|
+
// 2. Status 详细信息
|
|
167
|
+
// ==========================================================================
|
|
168
|
+
{
|
|
169
|
+
key: 'statusTopCode',
|
|
170
|
+
localPath: ['Response', 'Status', 'StatusCode'],
|
|
171
|
+
attributes: ['Value']
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
key: 'statusSecondCode',
|
|
175
|
+
localPath: ['Response', 'Status', 'StatusCode', 'StatusCode'],
|
|
176
|
+
attributes: ['Value']
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
key: 'statusMessage',
|
|
180
|
+
localPath: ['Response', 'Status', 'StatusMessage'],
|
|
181
|
+
attributes: [],
|
|
182
|
+
// 提示:如果 StatusMessage 有文本内容但 attributes 为空导致结果为空,
|
|
183
|
+
// 可能需要解析器支持提取 textContent。如果解析器仅支持属性,此处保持为空,结果可能为空字符串。
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
key: 'statusDetail',
|
|
187
|
+
localPath: ['Response', 'Status', 'StatusDetail'],
|
|
188
|
+
attributes: [],
|
|
189
|
+
context: true // 保持返回完整 XML 片段,除非解析器支持递归子字段定义
|
|
190
|
+
},
|
|
191
|
+
// ==========================================================================
|
|
192
|
+
// 3. Assertion 元数据
|
|
193
|
+
// ==========================================================================
|
|
194
|
+
{
|
|
195
|
+
key: 'assertionID',
|
|
196
|
+
localPath: ['Response', 'Assertion'],
|
|
197
|
+
attributes: ['ID']
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
key: 'assertionVersion',
|
|
201
|
+
localPath: ['Response', 'Assertion'],
|
|
202
|
+
attributes: ['Version']
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
key: 'assertionIssueInstant',
|
|
206
|
+
localPath: ['Response', 'Assertion'],
|
|
207
|
+
attributes: ['IssueInstant']
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
key: 'assertionSignature',
|
|
211
|
+
localPath: ['Response', 'Assertion', 'Signature'],
|
|
212
|
+
attributes: [],
|
|
213
|
+
context: true
|
|
214
|
+
},
|
|
215
|
+
// ==========================================================================
|
|
216
|
+
// 4. Conditions
|
|
217
|
+
// ==========================================================================
|
|
218
|
+
{
|
|
219
|
+
key: 'conditions',
|
|
220
|
+
localPath: ['Response', 'Assertion', 'Conditions'],
|
|
221
|
+
attributes: ['NotBefore', 'NotOnOrAfter']
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
key: 'audiences',
|
|
225
|
+
localPath: ['Response', 'Assertion', 'Conditions', 'AudienceRestriction', 'Audience'],
|
|
226
|
+
attributes: [],
|
|
227
|
+
listMode: true,
|
|
228
|
+
// 注意:Audience 通常只有文本内容,没有属性。如果解析器只提属性,这里会是空字符串。
|
|
229
|
+
// 如果解析器支持,它应该自动抓取 textContent。
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
key: 'oneTimeUse',
|
|
233
|
+
localPath: ['Response', 'Assertion', 'Conditions', 'OneTimeUse'],
|
|
234
|
+
attributes: []
|
|
235
|
+
// OneTimeUse 通常是一个空标签 <OneTimeUse/>,没有属性。
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
key: 'proxyRestriction',
|
|
239
|
+
localPath: ['Response', 'Assertion', 'Conditions', 'ProxyRestriction'],
|
|
240
|
+
attributes: ['Count']
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
key: 'proxyRestrictionAudiences',
|
|
244
|
+
localPath: ['Response', 'Assertion', 'Conditions', 'ProxyRestriction', 'Audience'],
|
|
245
|
+
attributes: [],
|
|
246
|
+
listMode: true
|
|
247
|
+
},
|
|
248
|
+
// ==========================================================================
|
|
249
|
+
// 5. Subject & NameID
|
|
250
|
+
// ==========================================================================
|
|
251
|
+
{
|
|
252
|
+
key: 'nameID',
|
|
253
|
+
localPath: ['Response', 'Assertion', 'Subject', 'NameID'],
|
|
254
|
+
attributes: [
|
|
255
|
+
'Format',
|
|
256
|
+
'NameQualifier',
|
|
257
|
+
'SPNameQualifier',
|
|
258
|
+
'SPProvidedID'
|
|
259
|
+
]
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
key: 'subjectConfirmation',
|
|
263
|
+
localPath: ['Response', 'Assertion', 'Subject', 'SubjectConfirmation', 'SubjectConfirmationData'],
|
|
264
|
+
attributes: [
|
|
265
|
+
'Recipient',
|
|
266
|
+
'InResponseTo',
|
|
267
|
+
'NotOnOrAfter',
|
|
268
|
+
'Address',
|
|
269
|
+
'NotBefore'
|
|
270
|
+
]
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
key: 'subjectBaseID',
|
|
274
|
+
localPath: ['Response', 'Assertion', 'Subject', 'BaseID'],
|
|
275
|
+
attributes: [], // BaseID 主要靠属性,如果这里有文本需求需额外处理
|
|
276
|
+
context: true
|
|
277
|
+
},
|
|
278
|
+
// ==========================================================================
|
|
279
|
+
// 6. AuthnStatement
|
|
280
|
+
// ==========================================================================
|
|
281
|
+
{
|
|
282
|
+
key: 'sessionIndex',
|
|
283
|
+
localPath: ['Response', 'Assertion', 'AuthnStatement'],
|
|
284
|
+
attributes: ['AuthnInstant', 'SessionNotOnOrAfter', 'SessionIndex']
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
key: 'authnContextClassRef',
|
|
288
|
+
localPath: ['Response', 'Assertion', 'AuthnStatement', 'AuthnContext', 'AuthnContextClassRef'],
|
|
289
|
+
attributes: []
|
|
290
|
+
// AuthnContextClassRef 通常是文本内容 (urn:...), 没有属性。
|
|
291
|
+
// 如果解析结果为空,说明解析器未提取文本内容。
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
key: 'authnContextDecl',
|
|
295
|
+
localPath: ['Response', 'Assertion', 'AuthnStatement', 'AuthnContext', 'AuthnContextDecl'],
|
|
296
|
+
attributes: [],
|
|
297
|
+
context: true
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
key: 'authenticatingAuthorities',
|
|
301
|
+
localPath: ['Response', 'Assertion', 'AuthnStatement', 'AuthnContext', 'AuthenticatingAuthority'],
|
|
302
|
+
attributes: [],
|
|
303
|
+
listMode: true
|
|
304
|
+
// AuthenticatingAuthority 通常也是文本内容。
|
|
305
|
+
},
|
|
306
|
+
// ==========================================================================
|
|
307
|
+
// 7. AttributeStatement (重点修复区域)
|
|
308
|
+
// ==========================================================================
|
|
309
|
+
{
|
|
310
|
+
key: 'attributes',
|
|
311
|
+
localPath: ['Response', 'Assertion', 'AttributeStatement', 'Attribute'],
|
|
312
|
+
index: ['Name'], // 使用 Name 属性作为 Key
|
|
313
|
+
attributePath: ['AttributeValue'], // 指向值的节点
|
|
314
|
+
attributes: [],
|
|
315
|
+
// 【关键修改】:
|
|
316
|
+
// 很多解析器在 attributePath 层级如果只配置 attributes 数组,会忽略文本内容。
|
|
317
|
+
// 如果可能,尝试添加一个特殊标记或确保解析器逻辑包含:
|
|
318
|
+
// if (node has text && no attributes matched) return text;
|
|
319
|
+
// 如果无法修改解析器内核,且 AttributeValue 只有文本没有属性,
|
|
320
|
+
// 这里的解析结果可能依然为空。
|
|
321
|
+
//
|
|
322
|
+
// 高级技巧:如果解析器支持,可以尝试将 '_' 或 'value' 加入 attributes 数组,
|
|
323
|
+
// 但这取决于 extractSpToll 的具体实现。
|
|
324
|
+
// 在此配置中,我们保持标准写法,依赖解析器对 attributePath 的默认文本提取。
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
key: 'encryptedAttributes',
|
|
328
|
+
localPath: ['Response', 'Assertion', 'AttributeStatement', 'EncryptedAttribute'],
|
|
329
|
+
attributes: [],
|
|
330
|
+
context: true
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
// ==========================================================================
|
|
334
|
+
// 9. Response Signature
|
|
335
|
+
// ==========================================================================
|
|
336
|
+
{
|
|
337
|
+
key: 'responseSignature',
|
|
338
|
+
localPath: ['Response', 'Signature'],
|
|
339
|
+
attributes: [],
|
|
340
|
+
context: true
|
|
341
|
+
}
|
|
342
|
+
];*/
|
|
77
343
|
export const logoutRequestFields = [
|
|
78
344
|
{ key: 'request', localPath: ['LogoutRequest'], attributes: ['ID', 'IssueInstant', 'Destination'] },
|
|
79
345
|
{ key: 'issuer', localPath: ['LogoutRequest', 'Issuer'], attributes: [] },
|
|
@@ -209,26 +475,185 @@ export const idpMetadataFields = [
|
|
|
209
475
|
}
|
|
210
476
|
];
|
|
211
477
|
// ============================================================================
|
|
212
|
-
//
|
|
478
|
+
// SAML2 SP 元数据完整字段配置 - 包含所有可选属性
|
|
213
479
|
// ============================================================================
|
|
214
480
|
export const spMetadataFields = [
|
|
215
|
-
|
|
216
|
-
{
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
{
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
481
|
+
// --- 1. 基础标识 ---
|
|
482
|
+
{
|
|
483
|
+
key: 'entityID',
|
|
484
|
+
localPath: ['EntityDescriptor'],
|
|
485
|
+
attributes: ['entityID']
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
key: 'validUntil',
|
|
489
|
+
localPath: ['EntityDescriptor'],
|
|
490
|
+
attributes: ['validUntil']
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
key: 'cacheDuration',
|
|
494
|
+
localPath: ['EntityDescriptor'],
|
|
495
|
+
attributes: ['cacheDuration']
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
key: 'ID',
|
|
499
|
+
localPath: ['EntityDescriptor'],
|
|
500
|
+
attributes: ['ID']
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
key: 'extensions',
|
|
504
|
+
localPath: ['EntityDescriptor', 'Extensions'],
|
|
505
|
+
attributes: [],
|
|
506
|
+
context: true
|
|
507
|
+
},
|
|
508
|
+
// --- 2. SPSSODescriptor 核心属性 ---
|
|
509
|
+
{
|
|
510
|
+
key: 'spSSODescriptor',
|
|
511
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor'],
|
|
512
|
+
attributes: [
|
|
513
|
+
'protocolSupportEnumeration',
|
|
514
|
+
'AuthnRequestsSigned', // SP 是否会对认证请求签名
|
|
515
|
+
'WantAssertionsSigned', // SP 是否希望接收签名的断言
|
|
516
|
+
'cacheDuration',
|
|
517
|
+
'validUntil',
|
|
518
|
+
'ID'
|
|
519
|
+
]
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
key: 'extensions',
|
|
523
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'Extensions'],
|
|
524
|
+
attributes: [],
|
|
525
|
+
context: true
|
|
526
|
+
},
|
|
527
|
+
// --- 3. 服务端点列表 (Endpoints) ---
|
|
528
|
+
{
|
|
529
|
+
key: 'assertionConsumerService',
|
|
530
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AssertionConsumerService'],
|
|
531
|
+
attributes: ['Binding', 'Location', 'index', 'isDefault', 'ResponseLocation'],
|
|
532
|
+
listMode: true
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
key: 'singleLogoutService',
|
|
536
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'SingleLogoutService'],
|
|
537
|
+
attributes: ['Binding', 'Location', 'index', 'isDefault', 'ResponseLocation'],
|
|
538
|
+
listMode: true
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
key: 'artifactResolutionService',
|
|
542
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'ArtifactResolutionService'],
|
|
543
|
+
attributes: ['Binding', 'Location', 'index', 'isDefault'],
|
|
544
|
+
listMode: true
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
key: 'manageNameIDService',
|
|
548
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'ManageNameIDService'],
|
|
549
|
+
attributes: ['Binding', 'Location', 'index', 'isDefault'],
|
|
550
|
+
listMode: true
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
key: 'nameIDMappingService',
|
|
554
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'NameIDMappingService'],
|
|
555
|
+
attributes: ['Binding', 'Location', 'index', 'isDefault'],
|
|
556
|
+
listMode: true
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
key: 'assertionIDRequestService',
|
|
560
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AssertionIDRequestService'],
|
|
561
|
+
attributes: ['Binding', 'Location', 'index', 'isDefault'],
|
|
562
|
+
listMode: true
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
key: 'attributeService',
|
|
566
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AttributeService'],
|
|
567
|
+
attributes: ['Binding', 'Location', 'index', 'isDefault'],
|
|
568
|
+
listMode: true
|
|
569
|
+
},
|
|
570
|
+
// --- 4. NameID 格式 ---
|
|
571
|
+
{
|
|
572
|
+
key: 'nameIDFormat',
|
|
573
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'NameIDFormat'],
|
|
574
|
+
attributes: [],
|
|
575
|
+
listMode: true
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
key: 'nameIDPolicy',
|
|
579
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'NameIDPolicy'],
|
|
580
|
+
attributes: ['Format', 'AllowCreate']
|
|
581
|
+
},
|
|
582
|
+
// --- 5. 属性消费服务 (AttributeConsumingService) ---
|
|
583
|
+
{
|
|
584
|
+
key: 'attributeConsumingService',
|
|
585
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AttributeConsumingService'],
|
|
586
|
+
attributes: ['index', 'isDefault', 'Name', 'NameFormat'],
|
|
587
|
+
listMode: true
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
key: 'serviceName',
|
|
591
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AttributeConsumingService', 'ServiceName'],
|
|
592
|
+
attributes: [],
|
|
593
|
+
listMode: true
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
key: 'serviceDescription',
|
|
597
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AttributeConsumingService', 'ServiceDescription'],
|
|
598
|
+
attributes: [],
|
|
599
|
+
listMode: true
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
key: 'requestedAttribute',
|
|
603
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AttributeConsumingService', 'RequestedAttribute'],
|
|
604
|
+
attributes: ['Name', 'NameFormat', 'FriendlyName', 'isRequired'],
|
|
605
|
+
listMode: true
|
|
606
|
+
},
|
|
607
|
+
// --- 6. 证书与密钥信息 ---
|
|
228
608
|
{ key: 'signingCert' },
|
|
229
609
|
{ key: 'encryptCert' },
|
|
230
610
|
{ key: 'signingKeyName' },
|
|
231
|
-
{ key: 'encryptionKeyName' }
|
|
611
|
+
{ key: 'encryptionKeyName' },
|
|
612
|
+
// --- 7. 组织信息 ---
|
|
613
|
+
{
|
|
614
|
+
key: 'organizationName',
|
|
615
|
+
localPath: ['EntityDescriptor', 'Organization', 'OrganizationName'],
|
|
616
|
+
attributes: [],
|
|
617
|
+
listMode: true
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
key: 'organizationDisplayName',
|
|
621
|
+
localPath: ['EntityDescriptor', 'Organization', 'OrganizationDisplayName'],
|
|
622
|
+
attributes: [],
|
|
623
|
+
listMode: true
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
key: 'organizationURL',
|
|
627
|
+
localPath: ['EntityDescriptor', 'Organization', 'OrganizationURL'],
|
|
628
|
+
attributes: [],
|
|
629
|
+
listMode: true
|
|
630
|
+
},
|
|
631
|
+
// --- 8. 联系人信息 ---
|
|
632
|
+
{
|
|
633
|
+
key: 'contactPerson',
|
|
634
|
+
localPath: ['EntityDescriptor', 'ContactPerson'],
|
|
635
|
+
attributes: ['contactType', 'company', 'givenName', 'surName', 'emailAddress', 'telephoneNumber'],
|
|
636
|
+
listMode: true
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
key: 'additionalMetadataLocation',
|
|
640
|
+
localPath: ['EntityDescriptor', 'AdditionalMetadataLocation'],
|
|
641
|
+
attributes: ['namespace'],
|
|
642
|
+
listMode: true
|
|
643
|
+
},
|
|
644
|
+
// --- 9. 其他可选元素 ---
|
|
645
|
+
{
|
|
646
|
+
key: 'affiliationDescriptor',
|
|
647
|
+
localPath: ['EntityDescriptor', 'AffiliationDescriptor'],
|
|
648
|
+
attributes: ['affiliationOwnerID', 'validUntil', 'cacheDuration', 'ID'],
|
|
649
|
+
context: true
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
key: 'roleDescriptor',
|
|
653
|
+
localPath: ['EntityDescriptor', 'RoleDescriptor'],
|
|
654
|
+
attributes: ['protocolSupportEnumeration', 'errorURL', 'cacheDuration', 'validUntil', 'ID'],
|
|
655
|
+
context: true
|
|
656
|
+
}
|
|
232
657
|
];
|
|
233
658
|
export function extract(context, fields) {
|
|
234
659
|
const { dom } = getContext();
|
|
@@ -419,9 +844,642 @@ export function extract(context, fields) {
|
|
|
419
844
|
return result;
|
|
420
845
|
}, {});
|
|
421
846
|
}
|
|
847
|
+
// 按照 SAML 2.0 Response 结构分类字段配置
|
|
848
|
+
// 按照 SAML 2.0 Response 结构分类字段配置,优化聚合
|
|
849
|
+
export const loginResponseFieldsFullList = [
|
|
850
|
+
// ==========================================================================
|
|
851
|
+
// 1. Response 根元素
|
|
852
|
+
// ==========================================================================
|
|
853
|
+
{
|
|
854
|
+
key: 'response',
|
|
855
|
+
localPath: ['Response'],
|
|
856
|
+
attributes: [
|
|
857
|
+
'ID',
|
|
858
|
+
'Version',
|
|
859
|
+
'IssueInstant',
|
|
860
|
+
'Destination',
|
|
861
|
+
'InResponseTo',
|
|
862
|
+
'Consent',
|
|
863
|
+
'RelayState'
|
|
864
|
+
]
|
|
865
|
+
},
|
|
866
|
+
// ==========================================================================
|
|
867
|
+
// 2. Status 详细信息
|
|
868
|
+
// ==========================================================================
|
|
869
|
+
{
|
|
870
|
+
key: 'status',
|
|
871
|
+
localPath: ['Response', 'Status'],
|
|
872
|
+
attributes: [],
|
|
873
|
+
context: true,
|
|
874
|
+
group: 'status',
|
|
875
|
+
parseCallback: (node) => {
|
|
876
|
+
if (!node)
|
|
877
|
+
return null;
|
|
878
|
+
const statusData = {};
|
|
879
|
+
// 提取顶级状态码
|
|
880
|
+
const statusCodeNodes = node.getElementsByTagNameNS('*', 'StatusCode');
|
|
881
|
+
if (statusCodeNodes.length > 0) {
|
|
882
|
+
statusData.topCode = statusCodeNodes[0].getAttribute('Value') || '';
|
|
883
|
+
// 检查是否有二级状态码
|
|
884
|
+
if (statusCodeNodes[0].getElementsByTagNameNS('*', 'StatusCode').length > 0) {
|
|
885
|
+
const secondStatusCode = statusCodeNodes[0].getElementsByTagNameNS('*', 'StatusCode')[0];
|
|
886
|
+
statusData.secondCode = secondStatusCode.getAttribute('Value') || '';
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// 提取状态消息
|
|
890
|
+
const statusMessageNodes = node.getElementsByTagNameNS('*', 'StatusMessage');
|
|
891
|
+
if (statusMessageNodes.length > 0) {
|
|
892
|
+
statusData.message = statusMessageNodes[0].textContent?.trim() || '';
|
|
893
|
+
}
|
|
894
|
+
// 提取状态详情
|
|
895
|
+
const statusDetailNodes = node.getElementsByTagNameNS('*', 'StatusDetail');
|
|
896
|
+
if (statusDetailNodes.length > 0) {
|
|
897
|
+
const detailNode = statusDetailNodes[0];
|
|
898
|
+
statusData.detail = detailNode.toString();
|
|
899
|
+
// 解析 StatusDetail 中的子元素
|
|
900
|
+
const extraInfoNodes = detailNode.getElementsByTagNameNS('*', 'ExtraInfo');
|
|
901
|
+
if (extraInfoNodes.length > 0) {
|
|
902
|
+
const sessionDurationNodes = extraInfoNodes[0].getElementsByTagNameNS('*', 'SessionDuration');
|
|
903
|
+
if (sessionDurationNodes.length > 0) {
|
|
904
|
+
statusData.sessionDuration = sessionDurationNodes[0].textContent?.trim() || null;
|
|
905
|
+
}
|
|
906
|
+
const mfaUsedNodes = extraInfoNodes[0].getElementsByTagNameNS('*', 'MFAUsed');
|
|
907
|
+
if (mfaUsedNodes.length > 0) {
|
|
908
|
+
statusData.mfaUsed = mfaUsedNodes[0].textContent?.trim() || null;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return statusData;
|
|
913
|
+
}
|
|
914
|
+
},
|
|
915
|
+
// ==========================================================================
|
|
916
|
+
// 3. Assertion 元数据
|
|
917
|
+
// ==========================================================================
|
|
918
|
+
{
|
|
919
|
+
key: 'assertion',
|
|
920
|
+
localPath: ['Response', 'Assertion'],
|
|
921
|
+
attributes: [
|
|
922
|
+
'ID',
|
|
923
|
+
'Version',
|
|
924
|
+
'IssueInstant'
|
|
925
|
+
]
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
key: 'assertionSignature',
|
|
929
|
+
localPath: ['Response', 'Assertion', 'Signature'],
|
|
930
|
+
attributes: [],
|
|
931
|
+
context: true,
|
|
932
|
+
parseCallback: (node) => {
|
|
933
|
+
if (!node)
|
|
934
|
+
return null;
|
|
935
|
+
const signatureData = {};
|
|
936
|
+
// 提取签名算法相关信息
|
|
937
|
+
const signatureMethods = node.getElementsByTagNameNS('*', 'SignatureMethod');
|
|
938
|
+
if (signatureMethods.length > 0) {
|
|
939
|
+
signatureData.signatureMethodAlgorithm = signatureMethods[0].getAttribute('Algorithm');
|
|
940
|
+
}
|
|
941
|
+
const canonicalizationMethods = node.getElementsByTagNameNS('*', 'CanonicalizationMethod');
|
|
942
|
+
if (canonicalizationMethods.length > 0) {
|
|
943
|
+
signatureData.canonicalizationMethodAlgorithm = canonicalizationMethods[0].getAttribute('Algorithm');
|
|
944
|
+
}
|
|
945
|
+
const digestMethods = node.getElementsByTagNameNS('*', 'DigestMethod');
|
|
946
|
+
if (digestMethods.length > 0) {
|
|
947
|
+
signatureData.digestMethodAlgorithm = digestMethods[0].getAttribute('Algorithm');
|
|
948
|
+
}
|
|
949
|
+
const digestValues = node.getElementsByTagNameNS('*', 'DigestValue');
|
|
950
|
+
if (digestValues.length > 0) {
|
|
951
|
+
signatureData.digestValue = digestValues[0].textContent?.trim();
|
|
952
|
+
}
|
|
953
|
+
const signatureValues = node.getElementsByTagNameNS('*', 'SignatureValue');
|
|
954
|
+
if (signatureValues.length > 0) {
|
|
955
|
+
signatureData.signatureValue = signatureValues[0].textContent?.trim();
|
|
956
|
+
}
|
|
957
|
+
const certificates = node.getElementsByTagNameNS('*', 'X509Certificate');
|
|
958
|
+
if (certificates.length > 0) {
|
|
959
|
+
signatureData.x509Certificate = certificates[0].textContent?.replace(/\s+/g, '');
|
|
960
|
+
}
|
|
961
|
+
return signatureData;
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
// ==========================================================================
|
|
965
|
+
// 4. Conditions
|
|
966
|
+
// ==========================================================================
|
|
967
|
+
{
|
|
968
|
+
key: 'conditions',
|
|
969
|
+
localPath: ['Response', 'Assertion', 'Conditions'],
|
|
970
|
+
attributes: ['NotBefore', 'NotOnOrAfter'],
|
|
971
|
+
context: true,
|
|
972
|
+
group: 'conditions',
|
|
973
|
+
parseCallback: (node) => {
|
|
974
|
+
if (!node)
|
|
975
|
+
return null;
|
|
976
|
+
const conditionsData = {};
|
|
977
|
+
// 基本条件
|
|
978
|
+
conditionsData.notBefore = node.getAttribute('NotBefore');
|
|
979
|
+
conditionsData.notOnOrAfter = node.getAttribute('NotOnOrAfter');
|
|
980
|
+
// Audience Restrictions
|
|
981
|
+
const audienceRestrictions = node.getElementsByTagNameNS('*', 'AudienceRestriction');
|
|
982
|
+
if (audienceRestrictions.length > 0) {
|
|
983
|
+
const audiences = [];
|
|
984
|
+
for (let i = 0; i < audienceRestrictions.length; i++) {
|
|
985
|
+
const audienceNodes = audienceRestrictions[i].getElementsByTagNameNS('*', 'Audience');
|
|
986
|
+
for (let j = 0; j < audienceNodes.length; j++) {
|
|
987
|
+
const audienceValue = audienceNodes[j].textContent?.trim();
|
|
988
|
+
if (audienceValue)
|
|
989
|
+
audiences.push(audienceValue);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
conditionsData.audiences = audiences;
|
|
993
|
+
}
|
|
994
|
+
// One Time Use
|
|
995
|
+
const oneTimeUses = node.getElementsByTagNameNS('*', 'OneTimeUse');
|
|
996
|
+
conditionsData.oneTimeUse = oneTimeUses.length > 0;
|
|
997
|
+
// Proxy Restriction
|
|
998
|
+
const proxyRestrictions = node.getElementsByTagNameNS('*', 'ProxyRestriction');
|
|
999
|
+
if (proxyRestrictions.length > 0) {
|
|
1000
|
+
const proxyRestriction = proxyRestrictions[0];
|
|
1001
|
+
conditionsData.proxyRestriction = proxyRestriction.getAttribute('Count');
|
|
1002
|
+
// Proxy Restriction Audiences
|
|
1003
|
+
const proxyAudiences = [];
|
|
1004
|
+
const proxyAudienceNodes = proxyRestriction.getElementsByTagNameNS('*', 'Audience');
|
|
1005
|
+
for (let i = 0; i < proxyAudienceNodes.length; i++) {
|
|
1006
|
+
const audienceValue = proxyAudienceNodes[i].textContent?.trim();
|
|
1007
|
+
if (audienceValue)
|
|
1008
|
+
proxyAudiences.push(audienceValue);
|
|
1009
|
+
}
|
|
1010
|
+
conditionsData.proxyRestrictionAudiences = proxyAudiences;
|
|
1011
|
+
}
|
|
1012
|
+
return conditionsData;
|
|
1013
|
+
}
|
|
1014
|
+
},
|
|
1015
|
+
// ==========================================================================
|
|
1016
|
+
// 5. Subject & NameID
|
|
1017
|
+
// ==========================================================================
|
|
1018
|
+
{
|
|
1019
|
+
key: 'subject',
|
|
1020
|
+
localPath: ['Response', 'Assertion', 'Subject'],
|
|
1021
|
+
attributes: [],
|
|
1022
|
+
context: true,
|
|
1023
|
+
group: 'subject',
|
|
1024
|
+
parseCallback: (node) => {
|
|
1025
|
+
if (!node)
|
|
1026
|
+
return null;
|
|
1027
|
+
const subjectData = {};
|
|
1028
|
+
// NameID
|
|
1029
|
+
const nameIDNodes = node.getElementsByTagNameNS('*', 'NameID');
|
|
1030
|
+
if (nameIDNodes.length > 0) {
|
|
1031
|
+
const nameIDNode = nameIDNodes[0];
|
|
1032
|
+
subjectData.nameID = {
|
|
1033
|
+
format: nameIDNode.getAttribute('Format'),
|
|
1034
|
+
nameQualifier: nameIDNode.getAttribute('NameQualifier'),
|
|
1035
|
+
spNameQualifier: nameIDNode.getAttribute('SPNameQualifier'),
|
|
1036
|
+
spProvidedID: nameIDNode.getAttribute('SPProvidedID'),
|
|
1037
|
+
value: nameIDNode.textContent?.trim() || ''
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
// Subject Confirmation
|
|
1041
|
+
const subjectConfirmations = node.getElementsByTagNameNS('*', 'SubjectConfirmation');
|
|
1042
|
+
if (subjectConfirmations.length > 0) {
|
|
1043
|
+
const confirmationData = {};
|
|
1044
|
+
const confirmationDataNodes = subjectConfirmations[0].getElementsByTagNameNS('*', 'SubjectConfirmationData');
|
|
1045
|
+
if (confirmationDataNodes.length > 0) {
|
|
1046
|
+
const dataNode = confirmationDataNodes[0];
|
|
1047
|
+
confirmationData.recipient = dataNode.getAttribute('Recipient');
|
|
1048
|
+
confirmationData.inResponseTo = dataNode.getAttribute('InResponseTo');
|
|
1049
|
+
confirmationData.notOnOrAfter = dataNode.getAttribute('NotOnOrAfter');
|
|
1050
|
+
confirmationData.address = dataNode.getAttribute('Address');
|
|
1051
|
+
confirmationData.notBefore = dataNode.getAttribute('NotBefore');
|
|
1052
|
+
}
|
|
1053
|
+
subjectData.subjectConfirmation = confirmationData;
|
|
1054
|
+
}
|
|
1055
|
+
// BaseID
|
|
1056
|
+
const baseIDNodes = node.getElementsByTagNameNS('*', 'BaseID');
|
|
1057
|
+
if (baseIDNodes.length > 0) {
|
|
1058
|
+
const baseIDNode = baseIDNodes[0];
|
|
1059
|
+
subjectData.baseID = {
|
|
1060
|
+
nameQualifier: baseIDNode.getAttribute('NameQualifier'),
|
|
1061
|
+
spNameQualifier: baseIDNode.getAttribute('SPNameQualifier'),
|
|
1062
|
+
value: baseIDNode.textContent?.trim() || baseIDNode.toString()
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
return subjectData;
|
|
1066
|
+
}
|
|
1067
|
+
},
|
|
1068
|
+
// ==========================================================================
|
|
1069
|
+
// 6. AuthnStatement
|
|
1070
|
+
// ==========================================================================
|
|
1071
|
+
{
|
|
1072
|
+
key: 'authnStatement',
|
|
1073
|
+
localPath: ['Response', 'Assertion', 'AuthnStatement'],
|
|
1074
|
+
attributes: ['AuthnInstant', 'SessionNotOnOrAfter', 'SessionIndex'],
|
|
1075
|
+
context: true,
|
|
1076
|
+
group: 'authnStatement',
|
|
1077
|
+
parseCallback: (node) => {
|
|
1078
|
+
if (!node)
|
|
1079
|
+
return null;
|
|
1080
|
+
const authnData = {};
|
|
1081
|
+
// 基本认证信息
|
|
1082
|
+
authnData.authnInstant = node.getAttribute('AuthnInstant');
|
|
1083
|
+
authnData.sessionNotOnOrAfter = node.getAttribute('SessionNotOnOrAfter');
|
|
1084
|
+
authnData.sessionIndex = node.getAttribute('SessionIndex');
|
|
1085
|
+
// 认证上下文
|
|
1086
|
+
const authnContextNodes = node.getElementsByTagNameNS('*', 'AuthnContext');
|
|
1087
|
+
if (authnContextNodes.length > 0) {
|
|
1088
|
+
const authnContextNode = authnContextNodes[0];
|
|
1089
|
+
// 认证上下文类引用
|
|
1090
|
+
const classRefNodes = authnContextNode.getElementsByTagNameNS('*', 'AuthnContextClassRef');
|
|
1091
|
+
if (classRefNodes.length > 0) {
|
|
1092
|
+
authnData.authnContextClassRef = classRefNodes[0].textContent?.trim() || '';
|
|
1093
|
+
}
|
|
1094
|
+
// 认证上下文声明
|
|
1095
|
+
const declNodes = authnContextNode.getElementsByTagNameNS('*', 'AuthnContextDecl');
|
|
1096
|
+
if (declNodes.length > 0) {
|
|
1097
|
+
const declNode = declNodes[0];
|
|
1098
|
+
authnData.authnContextDecl = {
|
|
1099
|
+
method: declNode.getElementsByTagNameNS('*', 'Method')[0]?.textContent?.trim() || '',
|
|
1100
|
+
strength: declNode.getElementsByTagNameNS('*', 'Strength')[0]?.textContent?.trim() || ''
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
// 认证机构
|
|
1104
|
+
const authorityNodes = authnContextNode.getElementsByTagNameNS('*', 'AuthenticatingAuthority');
|
|
1105
|
+
if (authorityNodes.length > 0) {
|
|
1106
|
+
authnData.authenticatingAuthorities = [];
|
|
1107
|
+
for (let i = 0; i < authorityNodes.length; i++) {
|
|
1108
|
+
const authorityValue = authorityNodes[i].textContent?.trim();
|
|
1109
|
+
if (authorityValue) {
|
|
1110
|
+
authnData.authenticatingAuthorities.push(authorityValue);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return authnData;
|
|
1116
|
+
}
|
|
1117
|
+
},
|
|
1118
|
+
// ==========================================================================
|
|
1119
|
+
// 7. AttributeStatement
|
|
1120
|
+
// ==========================================================================
|
|
1121
|
+
{
|
|
1122
|
+
key: 'attributeStatement',
|
|
1123
|
+
localPath: ['Response', 'Assertion', 'AttributeStatement'],
|
|
1124
|
+
attributes: [],
|
|
1125
|
+
context: true,
|
|
1126
|
+
group: 'attributeStatement',
|
|
1127
|
+
parseCallback: (node) => {
|
|
1128
|
+
if (!node)
|
|
1129
|
+
return null;
|
|
1130
|
+
const attributeData = {
|
|
1131
|
+
attributes: {},
|
|
1132
|
+
encryptedAttributes: null
|
|
1133
|
+
};
|
|
1134
|
+
// 提取普通属性
|
|
1135
|
+
const attributeNodes = node.getElementsByTagNameNS('*', 'Attribute');
|
|
1136
|
+
for (let i = 0; i < attributeNodes.length; i++) {
|
|
1137
|
+
const attrNode = attributeNodes[i];
|
|
1138
|
+
const name = attrNode.getAttribute('Name') || attrNode.getAttribute('FriendlyName');
|
|
1139
|
+
if (!name)
|
|
1140
|
+
continue;
|
|
1141
|
+
const values = [];
|
|
1142
|
+
const valueNodes = attrNode.getElementsByTagNameNS('*', 'AttributeValue');
|
|
1143
|
+
for (let j = 0; j < valueNodes.length; j++) {
|
|
1144
|
+
const value = valueNodes[j].textContent?.trim();
|
|
1145
|
+
if (value)
|
|
1146
|
+
values.push(value);
|
|
1147
|
+
}
|
|
1148
|
+
attributeData.attributes[name] = values;
|
|
1149
|
+
}
|
|
1150
|
+
// 提取加密属性
|
|
1151
|
+
const encryptedAttrNodes = node.getElementsByTagNameNS('*', 'EncryptedAttribute');
|
|
1152
|
+
if (encryptedAttrNodes.length > 0) {
|
|
1153
|
+
const encryptedAttrNode = encryptedAttrNodes[0];
|
|
1154
|
+
const encryptionMethods = encryptedAttrNode.getElementsByTagNameNS('*', 'EncryptionMethod');
|
|
1155
|
+
const cipherValues = encryptedAttrNode.getElementsByTagNameNS('*', 'CipherValue');
|
|
1156
|
+
attributeData.encryptedAttributes = {
|
|
1157
|
+
encryptionMethodAlgorithm: encryptionMethods.length > 0 ?
|
|
1158
|
+
encryptionMethods[0].getAttribute('Algorithm') : null,
|
|
1159
|
+
cipherValue: cipherValues.length > 0 ?
|
|
1160
|
+
cipherValues[0].textContent?.trim() : null
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
return attributeData;
|
|
1164
|
+
}
|
|
1165
|
+
},
|
|
1166
|
+
// ==========================================================================
|
|
1167
|
+
// 8. Response Signature
|
|
1168
|
+
// ==========================================================================
|
|
1169
|
+
{
|
|
1170
|
+
key: 'responseSignature',
|
|
1171
|
+
localPath: ['Response', 'Signature'],
|
|
1172
|
+
attributes: [],
|
|
1173
|
+
context: true,
|
|
1174
|
+
parseCallback: (node) => {
|
|
1175
|
+
if (!node)
|
|
1176
|
+
return null;
|
|
1177
|
+
const signatureData = {};
|
|
1178
|
+
// 提取签名算法相关信息
|
|
1179
|
+
const signatureMethods = node.getElementsByTagNameNS('*', 'SignatureMethod');
|
|
1180
|
+
if (signatureMethods.length > 0) {
|
|
1181
|
+
signatureData.signatureMethodAlgorithm = signatureMethods[0].getAttribute('Algorithm');
|
|
1182
|
+
}
|
|
1183
|
+
const canonicalizationMethods = node.getElementsByTagNameNS('*', 'CanonicalizationMethod');
|
|
1184
|
+
if (canonicalizationMethods.length > 0) {
|
|
1185
|
+
signatureData.canonicalizationMethodAlgorithm = canonicalizationMethods[0].getAttribute('Algorithm');
|
|
1186
|
+
}
|
|
1187
|
+
const digestMethods = node.getElementsByTagNameNS('*', 'DigestMethod');
|
|
1188
|
+
if (digestMethods.length > 0) {
|
|
1189
|
+
signatureData.digestMethodAlgorithm = digestMethods[0].getAttribute('Algorithm');
|
|
1190
|
+
}
|
|
1191
|
+
const digestValues = node.getElementsByTagNameNS('*', 'DigestValue');
|
|
1192
|
+
if (digestValues.length > 0) {
|
|
1193
|
+
signatureData.digestValue = digestValues[0].textContent?.trim();
|
|
1194
|
+
}
|
|
1195
|
+
const signatureValues = node.getElementsByTagNameNS('*', 'SignatureValue');
|
|
1196
|
+
if (signatureValues.length > 0) {
|
|
1197
|
+
signatureData.signatureValue = signatureValues[0].textContent?.trim();
|
|
1198
|
+
}
|
|
1199
|
+
const certificates = node.getElementsByTagNameNS('*', 'X509Certificate');
|
|
1200
|
+
if (certificates.length > 0) {
|
|
1201
|
+
signatureData.x509Certificate = certificates[0].textContent?.replace(/\s+/g, '');
|
|
1202
|
+
}
|
|
1203
|
+
return signatureData;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
];
|
|
1207
|
+
/**
|
|
1208
|
+
* 核心提取函数 - 优化版
|
|
1209
|
+
*/
|
|
1210
|
+
export function extractSpToll(context, fields) {
|
|
1211
|
+
const { dom } = getContext();
|
|
1212
|
+
if (!context || typeof context !== 'string' || context.trim() === '') {
|
|
1213
|
+
return {};
|
|
1214
|
+
}
|
|
1215
|
+
let rootDoc;
|
|
1216
|
+
try {
|
|
1217
|
+
rootDoc = dom.parseFromString(context, 'application/xml');
|
|
1218
|
+
}
|
|
1219
|
+
catch (e) {
|
|
1220
|
+
console.error('Failed to parse XML context:', e);
|
|
1221
|
+
return {};
|
|
1222
|
+
}
|
|
1223
|
+
if (rootDoc.getElementsByTagName('parsererror').length > 0) {
|
|
1224
|
+
console.error('XML Parse Error detected in context');
|
|
1225
|
+
return {};
|
|
1226
|
+
}
|
|
1227
|
+
return fields.reduce((result, field) => {
|
|
1228
|
+
const key = field.key;
|
|
1229
|
+
const localPath = field.localPath || [];
|
|
1230
|
+
const attributes = field.attributes || [];
|
|
1231
|
+
const isEntire = field.context;
|
|
1232
|
+
const shortcut = field.shortcut;
|
|
1233
|
+
const index = field.index;
|
|
1234
|
+
const attributePath = field.attributePath;
|
|
1235
|
+
const listMode = field.listMode;
|
|
1236
|
+
const parseCallback = field.parseCallback;
|
|
1237
|
+
let targetDoc = rootDoc;
|
|
1238
|
+
if (shortcut) {
|
|
1239
|
+
try {
|
|
1240
|
+
targetDoc = dom.parseFromString(shortcut, 'application/xml');
|
|
1241
|
+
}
|
|
1242
|
+
catch (e) {
|
|
1243
|
+
return result;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
// 特殊处理:证书和 KeyName
|
|
1247
|
+
if (key === 'signingCert' || key === 'encryptCert' || key === 'signingKeyName' || key === 'encryptionKeyName') {
|
|
1248
|
+
const isSigning = key.startsWith('signing');
|
|
1249
|
+
const useType = isSigning ? 'signing' : 'encryption';
|
|
1250
|
+
const isKeyName = key.endsWith('KeyName');
|
|
1251
|
+
const kdXPath = `//*[local-name(.)='KeyDescriptor' and @use='${useType}']`;
|
|
1252
|
+
let fullXPath = '';
|
|
1253
|
+
if (isKeyName) {
|
|
1254
|
+
fullXPath = `${kdXPath}/*[local-name(.)='KeyInfo']/*[local-name(.)='KeyName']/text()`;
|
|
1255
|
+
}
|
|
1256
|
+
else {
|
|
1257
|
+
fullXPath = `${kdXPath}/*[local-name(.)='KeyInfo']/*[local-name(.)='X509Data']/*[local-name(.)='X509Certificate']/text()`;
|
|
1258
|
+
}
|
|
1259
|
+
try {
|
|
1260
|
+
// @ts-ignore
|
|
1261
|
+
const nodes = select(fullXPath, targetDoc);
|
|
1262
|
+
if (isKeyName) {
|
|
1263
|
+
const keyNames = nodes.map((n) => n.nodeValue).filter(notEmpty);
|
|
1264
|
+
return { ...result, [key]: keyNames.length > 0 ? keyNames[0] : null };
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
const certs = nodes.map((n) => {
|
|
1268
|
+
const val = n.nodeValue || n.value;
|
|
1269
|
+
return val ? val.replace(/\r\n|\r|\n|\s/g, '') : null;
|
|
1270
|
+
}).filter(notEmpty);
|
|
1271
|
+
return { ...result, [key]: certs.length > 0 ? certs[0] : null };
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
catch (e) {
|
|
1275
|
+
return { ...result, [key]: null };
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
// 多路径处理
|
|
1279
|
+
if (Array.isArray(localPath) && localPath.length > 0 && Array.isArray(localPath[0])) {
|
|
1280
|
+
const multiXPaths = localPath.map(path => `${buildAbsoluteXPath(path)}/text()`).join(' | ');
|
|
1281
|
+
try {
|
|
1282
|
+
// @ts-ignore
|
|
1283
|
+
const nodes = select(multiXPaths, targetDoc);
|
|
1284
|
+
return { ...result, [key]: uniq(nodes.map((n) => n.nodeValue).filter(notEmpty)) };
|
|
1285
|
+
}
|
|
1286
|
+
catch (e) {
|
|
1287
|
+
return { ...result, [key]: [] };
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
const currentLocalPath = localPath;
|
|
1291
|
+
if (currentLocalPath.length === 0 && !isEntire) {
|
|
1292
|
+
return { ...result, [key]: null };
|
|
1293
|
+
}
|
|
1294
|
+
const baseXPath = buildAbsoluteXPath(currentLocalPath);
|
|
1295
|
+
// 列表模式处理
|
|
1296
|
+
if (listMode) {
|
|
1297
|
+
try {
|
|
1298
|
+
// @ts-ignore
|
|
1299
|
+
const nodes = select(baseXPath, targetDoc);
|
|
1300
|
+
if (parseCallback) {
|
|
1301
|
+
// 使用自定义回调函数处理列表
|
|
1302
|
+
return { ...result, [key]: parseCallback(nodes) };
|
|
1303
|
+
}
|
|
1304
|
+
// 通用列表处理
|
|
1305
|
+
const resultList = nodes.map((node) => {
|
|
1306
|
+
if (attributes.length > 0) {
|
|
1307
|
+
const attrResult = {};
|
|
1308
|
+
attributes.forEach(attr => {
|
|
1309
|
+
if (node.getAttribute) {
|
|
1310
|
+
const val = node.getAttribute(attr);
|
|
1311
|
+
if (val !== null && val !== undefined) {
|
|
1312
|
+
attrResult[camelCase(attr, { locale: 'en-us' })] = val;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
return attrResult;
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
// 当没有属性时,获取文本内容
|
|
1320
|
+
let text = node.textContent;
|
|
1321
|
+
if (!text && node.firstChild)
|
|
1322
|
+
text = node.firstChild.nodeValue;
|
|
1323
|
+
const trimmed = text ? text.trim() : '';
|
|
1324
|
+
return trimmed;
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
return { ...result, [key]: resultList };
|
|
1328
|
+
}
|
|
1329
|
+
catch (e) {
|
|
1330
|
+
console.error(`Error extracting list ${key}:`, e);
|
|
1331
|
+
return { ...result, [key]: [] };
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
// 属性聚合 (Index + AttributePath)
|
|
1335
|
+
if (index && attributePath) {
|
|
1336
|
+
try {
|
|
1337
|
+
const indexPath = buildAttributeXPath(index);
|
|
1338
|
+
const fullLocalXPath = `${baseXPath}${indexPath}`;
|
|
1339
|
+
// @ts-ignore
|
|
1340
|
+
const parentNodes = select(baseXPath, targetDoc);
|
|
1341
|
+
// @ts-ignore
|
|
1342
|
+
const parentAttributes = select(fullLocalXPath, targetDoc).map((n) => n.value);
|
|
1343
|
+
const childXPath = buildAbsoluteXPath([last(currentLocalPath)].concat(attributePath));
|
|
1344
|
+
const childAttributeXPath = buildAttributeXPath(attributes);
|
|
1345
|
+
const fullChildXPath = `${childXPath}${childAttributeXPath}`;
|
|
1346
|
+
const childAttributes = parentNodes.map((node) => {
|
|
1347
|
+
try {
|
|
1348
|
+
const nodeStr = node.toString();
|
|
1349
|
+
if (!nodeStr)
|
|
1350
|
+
return null;
|
|
1351
|
+
const nodeDoc = dom.parseFromString(nodeStr, 'application/xml');
|
|
1352
|
+
if (attributes.length === 0) {
|
|
1353
|
+
// @ts-ignore
|
|
1354
|
+
const childValues = select(fullChildXPath, nodeDoc).map((n) => n.nodeValue);
|
|
1355
|
+
return childValues.length === 1 ? childValues[0] : childValues;
|
|
1356
|
+
}
|
|
1357
|
+
if (attributes.length > 0) {
|
|
1358
|
+
// @ts-ignore
|
|
1359
|
+
const childValues = select(fullChildXPath, nodeDoc).map((n) => n.value);
|
|
1360
|
+
return childValues.length === 1 ? childValues[0] : childValues;
|
|
1361
|
+
}
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
catch (e) {
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
const obj = zipObject(parentAttributes, childAttributes, false);
|
|
1369
|
+
return { ...result, [key]: obj };
|
|
1370
|
+
}
|
|
1371
|
+
catch (e) {
|
|
1372
|
+
return { ...result, [key]: {} };
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
// 获取整个节点 (Context)
|
|
1376
|
+
if (isEntire) {
|
|
1377
|
+
try {
|
|
1378
|
+
// @ts-ignore
|
|
1379
|
+
const node = select(baseXPath, targetDoc);
|
|
1380
|
+
let value = null;
|
|
1381
|
+
if (node.length === 1) {
|
|
1382
|
+
value = node[0].toString();
|
|
1383
|
+
// 如果有自定义解析回调,使用它
|
|
1384
|
+
if (parseCallback) {
|
|
1385
|
+
const parsedValue = parseCallback(node[0]);
|
|
1386
|
+
return { ...result, [key]: parsedValue };
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (node.length > 1) {
|
|
1390
|
+
value = node.map((n) => n.toString());
|
|
1391
|
+
}
|
|
1392
|
+
return { ...result, [key]: value };
|
|
1393
|
+
}
|
|
1394
|
+
catch (e) {
|
|
1395
|
+
return { ...result, [key]: null };
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
// 多属性对象
|
|
1399
|
+
if (attributes.length > 1 && !listMode) {
|
|
1400
|
+
try {
|
|
1401
|
+
// @ts-ignore
|
|
1402
|
+
const baseNodeList = select(baseXPath, targetDoc);
|
|
1403
|
+
if (baseNodeList.length === 0)
|
|
1404
|
+
return { ...result, [key]: null };
|
|
1405
|
+
const attributeValues = baseNodeList.map((node) => {
|
|
1406
|
+
const nodeStr = node.toString();
|
|
1407
|
+
if (!nodeStr)
|
|
1408
|
+
return {};
|
|
1409
|
+
const nodeDoc = dom.parseFromString(nodeStr, 'application/xml');
|
|
1410
|
+
const childXPath = `${buildAbsoluteXPath([last(currentLocalPath)])}${buildAttributeXPath(attributes)}`;
|
|
1411
|
+
// @ts-ignore
|
|
1412
|
+
const values = select(childXPath, nodeDoc).reduce((r, n) => {
|
|
1413
|
+
if (n.name && n.value !== undefined)
|
|
1414
|
+
r[camelCase(n.name, { locale: 'en-us' })] = n.value;
|
|
1415
|
+
return r;
|
|
1416
|
+
}, {});
|
|
1417
|
+
return values;
|
|
1418
|
+
});
|
|
1419
|
+
return { ...result, [key]: attributeValues.length === 1 ? attributeValues[0] : attributeValues };
|
|
1420
|
+
}
|
|
1421
|
+
catch (e) {
|
|
1422
|
+
return { ...result, [key]: null };
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
// 单个属性
|
|
1426
|
+
if (attributes.length === 1 && !listMode) {
|
|
1427
|
+
try {
|
|
1428
|
+
const fullPath = `${baseXPath}${buildAttributeXPath(attributes)}`;
|
|
1429
|
+
// @ts-ignore
|
|
1430
|
+
const attributeValues = select(fullPath, targetDoc).map((n) => n.value);
|
|
1431
|
+
return { ...result, [key]: attributeValues[0] || null };
|
|
1432
|
+
}
|
|
1433
|
+
catch (e) {
|
|
1434
|
+
return { ...result, [key]: null };
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
// 纯文本内容
|
|
1438
|
+
if (attributes.length === 0 && !listMode) {
|
|
1439
|
+
try {
|
|
1440
|
+
// @ts-ignore
|
|
1441
|
+
const node = select(baseXPath, targetDoc);
|
|
1442
|
+
if (parseCallback) {
|
|
1443
|
+
// 使用自定义回调函数处理单个节点
|
|
1444
|
+
return { ...result, [key]: parseCallback(node[0]) };
|
|
1445
|
+
}
|
|
1446
|
+
let attributeValue = null;
|
|
1447
|
+
if (node.length === 1) {
|
|
1448
|
+
const fullPath = `string(${baseXPath})`;
|
|
1449
|
+
// @ts-ignore
|
|
1450
|
+
attributeValue = select(fullPath, targetDoc);
|
|
1451
|
+
}
|
|
1452
|
+
if (node.length > 1) {
|
|
1453
|
+
attributeValue = node.filter((n) => n.firstChild)
|
|
1454
|
+
.map((n) => {
|
|
1455
|
+
let t = n.firstChild.nodeValue;
|
|
1456
|
+
if (!t && n.textContent)
|
|
1457
|
+
t = n.textContent;
|
|
1458
|
+
return t ? t.trim() : null;
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
return { ...result, [key]: attributeValue };
|
|
1462
|
+
}
|
|
1463
|
+
catch (e) {
|
|
1464
|
+
return { ...result, [key]: null };
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return result;
|
|
1468
|
+
}, {});
|
|
1469
|
+
}
|
|
1470
|
+
// 新增函数:调用 extractSpToll 提取 SP 数据
|
|
1471
|
+
export function extractSpData(context) {
|
|
1472
|
+
return extractSpToll(context, spMetadataFields);
|
|
1473
|
+
}
|
|
422
1474
|
export function extractIdp(context) {
|
|
423
1475
|
return extract(context, idpMetadataFields);
|
|
424
1476
|
}
|
|
425
1477
|
export function extractSp(context) {
|
|
426
1478
|
return extract(context, spMetadataFields);
|
|
427
1479
|
}
|
|
1480
|
+
export function extractAuthRequest(context) {
|
|
1481
|
+
return extract(context, loginRequestFields);
|
|
1482
|
+
}
|
|
1483
|
+
export function extractResponse(context, ass) {
|
|
1484
|
+
return extractSpToll(context, loginResponseFieldsFullList);
|
|
1485
|
+
}
|