samlesa 3.2.2 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/binding-post.js +9 -0
- package/build/src/entity.js +10 -12
- package/build/src/extractor.js +275 -283
- package/build/src/flow.js +10 -1
- package/build/src/libsaml.js +2 -46
- package/build/src/urn.js +78 -15
- package/package.json +1 -1
- package/types/src/binding-post.d.ts.map +1 -1
- package/types/src/binding-redirect.d.ts.map +1 -1
- package/types/src/entity.d.ts +11 -8
- package/types/src/entity.d.ts.map +1 -1
- package/types/src/extractor.d.ts +14 -40
- package/types/src/extractor.d.ts.map +1 -1
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/libsaml.d.ts +0 -9
- package/types/src/libsaml.d.ts.map +1 -1
- package/types/src/urn.d.ts +48 -14
- package/types/src/urn.d.ts.map +1 -1
package/build/src/extractor.js
CHANGED
|
@@ -3,6 +3,8 @@ import { uniq, last, zipObject, notEmpty } from './utility.js';
|
|
|
3
3
|
import { getContext } from './api.js';
|
|
4
4
|
import camelCase from 'camelcase';
|
|
5
5
|
function buildAbsoluteXPath(paths) {
|
|
6
|
+
if (!paths || paths.length === 0)
|
|
7
|
+
return '';
|
|
6
8
|
return paths.reduce((currentPath, name) => {
|
|
7
9
|
let appendedPath = currentPath;
|
|
8
10
|
const isWildcard = name.startsWith('~');
|
|
@@ -10,14 +12,14 @@ function buildAbsoluteXPath(paths) {
|
|
|
10
12
|
const pathName = name.replace('~', '');
|
|
11
13
|
appendedPath = currentPath + `/*[contains(local-name(), '${pathName}')]`;
|
|
12
14
|
}
|
|
13
|
-
|
|
15
|
+
else {
|
|
14
16
|
appendedPath = currentPath + `/*[local-name(.)='${name}']`;
|
|
15
17
|
}
|
|
16
18
|
return appendedPath;
|
|
17
19
|
}, '');
|
|
18
20
|
}
|
|
19
21
|
function buildAttributeXPath(attributes) {
|
|
20
|
-
if (attributes.length === 0) {
|
|
22
|
+
if (!attributes || attributes.length === 0) {
|
|
21
23
|
return '/text()';
|
|
22
24
|
}
|
|
23
25
|
if (attributes.length === 1) {
|
|
@@ -26,355 +28,359 @@ function buildAttributeXPath(attributes) {
|
|
|
26
28
|
const filters = attributes.map(attribute => `name()='${attribute}'`).join(' or ');
|
|
27
29
|
return `/@*[${filters}]`;
|
|
28
30
|
}
|
|
31
|
+
// ... (其他字段配置如 loginRequestFields 等保持不变,为节省篇幅此处省略,请保留你原有的所有 fields 定义) ...
|
|
32
|
+
// 为了完整性,这里假设你保留了之前所有的 fields 定义 (loginRequestFields, idpMetadataFields 等)
|
|
33
|
+
// 重点修正下方的 spMetadataFields 和 extract 函数
|
|
29
34
|
export const loginRequestFields = [
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
{
|
|
36
|
-
key: 'issuer',
|
|
37
|
-
localPath: ['AuthnRequest', 'Issuer'],
|
|
38
|
-
attributes: []
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
key: 'nameIDPolicy',
|
|
42
|
-
localPath: ['AuthnRequest', 'NameIDPolicy'],
|
|
43
|
-
attributes: ['Format', 'AllowCreate']
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
key: 'authnContextClassRef',
|
|
47
|
-
localPath: ['AuthnRequest', 'AuthnContextClassRef'],
|
|
48
|
-
attributes: []
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
key: 'signature',
|
|
52
|
-
localPath: ['AuthnRequest', 'Signature'],
|
|
53
|
-
attributes: [],
|
|
54
|
-
context: true
|
|
55
|
-
}
|
|
35
|
+
{ key: 'request', localPath: ['AuthnRequest'], attributes: ['ID', 'IssueInstant', 'Destination', 'AssertionConsumerServiceURL', 'ProtocolBinding', 'ForceAuthn', 'IsPassive', 'AssertionConsumerServiceIndex', 'AttributeConsumingServiceIndex'] },
|
|
36
|
+
{ key: 'issuer', localPath: ['AuthnRequest', 'Issuer'], attributes: [] },
|
|
37
|
+
{ key: 'nameIDPolicy', localPath: ['AuthnRequest', 'NameIDPolicy'], attributes: ['Format', 'AllowCreate'] },
|
|
38
|
+
{ key: 'authnContextClassRef', localPath: ['AuthnRequest', 'AuthnContextClassRef'], attributes: [] },
|
|
39
|
+
{ key: 'signature', localPath: ['AuthnRequest', 'Signature'], attributes: [], context: true }
|
|
56
40
|
];
|
|
57
41
|
export const artifactResolveFields = [
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
key: 'issuer', localPath: ['ArtifactResolve', 'Issuer'], attributes: []
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
key: 'Artifact', localPath: ['ArtifactResolve', 'Artifact'], attributes: []
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
key: 'signature', localPath: ['ArtifactResolve', 'Signature'], attributes: [], context: true
|
|
71
|
-
},
|
|
42
|
+
{ key: 'request', localPath: ['ArtifactResolve'], attributes: ['ID', 'IssueInstant', 'Version'] },
|
|
43
|
+
{ key: 'issuer', localPath: ['ArtifactResolve', 'Issuer'], attributes: [] },
|
|
44
|
+
{ key: 'Artifact', localPath: ['ArtifactResolve', 'Artifact'], attributes: [] },
|
|
45
|
+
{ key: 'signature', localPath: ['ArtifactResolve', 'Signature'], attributes: [], context: true },
|
|
72
46
|
];
|
|
73
47
|
export const artifactResponseFields = [
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
key: 'issuer', localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Issuer'], attributes: []
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
key: 'Artifact', localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Artifact'], attributes: []
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
key: 'signature', localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Signature'], attributes: [], context: true
|
|
87
|
-
},
|
|
48
|
+
{ key: 'request', localPath: ['Envelope', 'Body', 'ArtifactResolve'], attributes: ['ID', 'IssueInstant', 'Version'] },
|
|
49
|
+
{ key: 'issuer', localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Issuer'], attributes: [] },
|
|
50
|
+
{ key: 'Artifact', localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Artifact'], attributes: [] },
|
|
51
|
+
{ key: 'signature', localPath: ['Envelope', 'Body', 'ArtifactResolve', 'Signature'], attributes: [], context: true },
|
|
88
52
|
];
|
|
89
|
-
// support two-tiers status code
|
|
90
53
|
export const loginResponseStatusFields = [
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
localPath: ['Response', 'Status', 'StatusCode'],
|
|
94
|
-
attributes: ['Value'],
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
key: 'second',
|
|
98
|
-
localPath: ['Response', 'Status', 'StatusCode', 'StatusCode'],
|
|
99
|
-
attributes: ['Value'],
|
|
100
|
-
}
|
|
54
|
+
{ key: 'top', localPath: ['Response', 'Status', 'StatusCode'], attributes: ['Value'] },
|
|
55
|
+
{ key: 'second', localPath: ['Response', 'Status', 'StatusCode', 'StatusCode'], attributes: ['Value'] }
|
|
101
56
|
];
|
|
102
|
-
// support two-tiers status code
|
|
103
57
|
export const loginArtifactResponseStatusFields = [
|
|
104
|
-
{
|
|
105
|
-
|
|
106
|
-
localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Status', 'StatusCode'],
|
|
107
|
-
attributes: ['Value'],
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
key: 'second',
|
|
111
|
-
localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Status', 'StatusCode', 'StatusCode'],
|
|
112
|
-
attributes: ['Value'],
|
|
113
|
-
}
|
|
58
|
+
{ key: 'top', localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Status', 'StatusCode'], attributes: ['Value'] },
|
|
59
|
+
{ key: 'second', localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Status', 'StatusCode', 'StatusCode'], attributes: ['Value'] }
|
|
114
60
|
];
|
|
115
|
-
// support two-tiers status code
|
|
116
61
|
export const logoutResponseStatusFields = [
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
localPath: ['LogoutResponse', 'Status', 'StatusCode'],
|
|
120
|
-
attributes: ['Value']
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
key: 'second',
|
|
124
|
-
localPath: ['LogoutResponse', 'Status', 'StatusCode', 'StatusCode'],
|
|
125
|
-
attributes: ['Value'],
|
|
126
|
-
}
|
|
62
|
+
{ key: 'top', localPath: ['LogoutResponse', 'Status', 'StatusCode'], attributes: ['Value'] },
|
|
63
|
+
{ key: 'second', localPath: ['LogoutResponse', 'Status', 'StatusCode', 'StatusCode'], attributes: ['Value'] }
|
|
127
64
|
];
|
|
128
65
|
export const loginResponseFields = assertion => [
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
66
|
+
{ key: 'conditions', localPath: ['Assertion', 'Conditions'], attributes: ['NotBefore', 'NotOnOrAfter'], shortcut: assertion },
|
|
67
|
+
{ key: 'response', localPath: ['Response'], attributes: ['ID', 'IssueInstant', 'Destination', 'InResponseTo', 'Version'] },
|
|
68
|
+
{ key: 'audience', localPath: ['Assertion', 'Conditions', 'AudienceRestriction', 'Audience'], attributes: [], shortcut: assertion },
|
|
69
|
+
{ key: 'issuer', localPath: ['Assertion', 'Issuer'], attributes: [], shortcut: assertion },
|
|
70
|
+
{ key: 'nameID', localPath: ['Assertion', 'Subject', 'NameID'], attributes: [], shortcut: assertion },
|
|
71
|
+
{ key: 'sessionIndex', localPath: ['Assertion', 'AuthnStatement'], attributes: ['AuthnInstant', 'SessionNotOnOrAfter', 'SessionIndex'], shortcut: assertion },
|
|
72
|
+
{ key: 'attributes', localPath: ['Assertion', 'AttributeStatement', 'Attribute'], index: ['Name'], attributePath: ['AttributeValue'], attributes: [], shortcut: assertion },
|
|
73
|
+
{ key: 'subjectConfirmation', localPath: ['Assertion', 'Subject', 'SubjectConfirmation', 'SubjectConfirmationData'], attributes: ['Recipient', 'InResponseTo', 'NotOnOrAfter'], shortcut: assertion },
|
|
74
|
+
{ key: 'oneTimeUse', localPath: ['Assertion', 'Conditions', 'OneTimeUse'], attributes: [], shortcut: assertion },
|
|
75
|
+
{ key: 'status', localPath: ['Response', 'Status', 'StatusCode'], attributes: ['Value'] },
|
|
76
|
+
];
|
|
77
|
+
export const logoutRequestFields = [
|
|
78
|
+
{ key: 'request', localPath: ['LogoutRequest'], attributes: ['ID', 'IssueInstant', 'Destination'] },
|
|
79
|
+
{ key: 'issuer', localPath: ['LogoutRequest', 'Issuer'], attributes: [] },
|
|
80
|
+
{ key: 'nameID', localPath: ['LogoutRequest', 'NameID'], attributes: [] },
|
|
81
|
+
{ key: 'sessionIndex', localPath: ['LogoutRequest', 'SessionIndex'], attributes: [] },
|
|
82
|
+
{ key: 'signature', localPath: ['LogoutRequest', 'Signature'], attributes: [], context: true }
|
|
83
|
+
];
|
|
84
|
+
export const logoutResponseFields = [
|
|
85
|
+
{ key: 'response', localPath: ['LogoutResponse'], attributes: ['ID', 'Destination', 'InResponseTo'] },
|
|
86
|
+
{ key: 'issuer', localPath: ['LogoutResponse', 'Issuer'], attributes: [] },
|
|
87
|
+
{ key: 'signature', localPath: ['LogoutResponse', 'Signature'], attributes: [], context: true }
|
|
88
|
+
];
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// 增强版:IdP 元数据提取字段配置
|
|
91
|
+
// ============================================================================
|
|
92
|
+
export const idpMetadataFields = [
|
|
93
|
+
// --- 1. 基础标识 ---
|
|
94
|
+
{
|
|
95
|
+
key: 'entityID',
|
|
96
|
+
localPath: ['EntityDescriptor'],
|
|
97
|
+
attributes: ['entityID']
|
|
134
98
|
},
|
|
135
99
|
{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
100
|
+
// 可选:提取整个 EntityDescriptor 的 validUntil 和 cacheDuration
|
|
101
|
+
key: 'entityDescriptor',
|
|
102
|
+
localPath: ['EntityDescriptor'],
|
|
103
|
+
attributes: ['validUntil', 'cacheDuration']
|
|
139
104
|
},
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
105
|
+
// --- 2. IDPSSODescriptor 核心属性 ---
|
|
106
|
+
{
|
|
107
|
+
key: 'idpSSODescriptor',
|
|
108
|
+
localPath: ['EntityDescriptor', 'IDPSSODescriptor'],
|
|
109
|
+
attributes: [
|
|
110
|
+
'protocolSupportEnumeration',
|
|
111
|
+
'WantAuthnRequestsSigned', // IdP 是否希望 SP 对请求签名
|
|
112
|
+
'cacheDuration'
|
|
113
|
+
]
|
|
145
114
|
},
|
|
146
|
-
//
|
|
147
|
-
// key: 'issuer',
|
|
148
|
-
// localPath: ['Response', 'Issuer'],
|
|
149
|
-
// attributes: []
|
|
150
|
-
// },
|
|
115
|
+
// --- 3. 服务端点列表 (Endpoints) ---
|
|
151
116
|
{
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
117
|
+
// 单点登录服务 (SSO) - 核心
|
|
118
|
+
key: 'singleSignOnService',
|
|
119
|
+
localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'SingleSignOnService'],
|
|
120
|
+
attributes: ['Binding', 'Location', 'ResponseLocation'], // ResponseLocation 用于某些绑定
|
|
121
|
+
listMode: true
|
|
156
122
|
},
|
|
157
123
|
{
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
124
|
+
// 单点注销服务 (SLO)
|
|
125
|
+
key: 'singleLogoutService',
|
|
126
|
+
localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'SingleLogoutService'],
|
|
127
|
+
attributes: ['Binding', 'Location'],
|
|
128
|
+
listMode: true
|
|
162
129
|
},
|
|
163
130
|
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
131
|
+
// Artifact 解析服务 (如果使用 Artifact 绑定)
|
|
132
|
+
key: 'artifactResolutionService',
|
|
133
|
+
localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'ArtifactResolutionService'],
|
|
134
|
+
attributes: ['Binding', 'Location', 'index', 'isDefault'],
|
|
135
|
+
listMode: true
|
|
168
136
|
},
|
|
169
137
|
{
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
shortcut: assertion
|
|
138
|
+
// ManageNameID 服务
|
|
139
|
+
key: 'manageNameIDService',
|
|
140
|
+
localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'ManageNameIDService'],
|
|
141
|
+
attributes: ['Binding', 'Location'],
|
|
142
|
+
listMode: true
|
|
176
143
|
},
|
|
144
|
+
// --- 4. 名称 ID 格式 (NameID Formats) ---
|
|
177
145
|
{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
146
|
+
// 提取所有支持的 NameID 格式 (文本内容)
|
|
147
|
+
key: 'nameIDFormat',
|
|
148
|
+
localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'NameIDFormat'],
|
|
149
|
+
attributes: []
|
|
150
|
+
// 注意:如果 extract 函数未完全支持 text() 的 listMode,这里可能只返回第一个。
|
|
151
|
+
// 如果需要数组,需确保 extract 逻辑完善,或者此处暂时只取主要的一个。
|
|
182
152
|
},
|
|
183
153
|
{
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
154
|
+
// 获取主要的 NameID Policy 格式 (如果有显式声明)
|
|
155
|
+
key: 'nameIDPolicyFormat',
|
|
156
|
+
localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'NameIDPolicy'],
|
|
157
|
+
attributes: ['Format']
|
|
188
158
|
},
|
|
159
|
+
// --- 5. 组织信息 (Organization) ---
|
|
189
160
|
{
|
|
190
|
-
key: '
|
|
191
|
-
localPath: ['
|
|
192
|
-
attributes: [
|
|
161
|
+
key: 'organizationName',
|
|
162
|
+
localPath: ['EntityDescriptor', 'Organization', 'OrganizationName'],
|
|
163
|
+
attributes: [] // 取文本内容
|
|
193
164
|
},
|
|
194
|
-
];
|
|
195
|
-
export const logoutRequestFields = [
|
|
196
165
|
{
|
|
197
|
-
key: '
|
|
198
|
-
localPath: ['
|
|
199
|
-
attributes: [
|
|
166
|
+
key: 'organizationDisplayName',
|
|
167
|
+
localPath: ['EntityDescriptor', 'Organization', 'OrganizationDisplayName'],
|
|
168
|
+
attributes: []
|
|
200
169
|
},
|
|
201
170
|
{
|
|
202
|
-
key: '
|
|
203
|
-
localPath: ['
|
|
171
|
+
key: 'organizationURL',
|
|
172
|
+
localPath: ['EntityDescriptor', 'Organization', 'OrganizationURL'],
|
|
204
173
|
attributes: []
|
|
205
174
|
},
|
|
175
|
+
// --- 6. 联系人信息 (ContactPerson) ---
|
|
206
176
|
{
|
|
207
|
-
key: '
|
|
208
|
-
localPath: ['
|
|
209
|
-
attributes: []
|
|
177
|
+
key: 'contactPerson',
|
|
178
|
+
localPath: ['EntityDescriptor', 'ContactPerson'],
|
|
179
|
+
attributes: ['contactType'],
|
|
180
|
+
listMode: true
|
|
181
|
+
// 局限:目前只能提取 contactType。如需提取 Email/GivenName,需扩展 extract 逻辑。
|
|
210
182
|
},
|
|
183
|
+
// --- 7. 证书与密钥信息 (Certificates & Keys) ---
|
|
184
|
+
// 这些 key 会触发 extract 函数内部的硬编码逻辑,自动查找对应 @use 的证书
|
|
185
|
+
// 7.1 签名证书 (IdP 用来签名响应/断言)
|
|
211
186
|
{
|
|
212
|
-
key: '
|
|
213
|
-
localPath
|
|
214
|
-
attributes: []
|
|
187
|
+
key: 'signingCert'
|
|
188
|
+
// localPath 和 attributes 将被内部逻辑忽略
|
|
215
189
|
},
|
|
190
|
+
// 7.2 加密证书 (IdP 用来加密断言中的敏感信息,如果有)
|
|
216
191
|
{
|
|
217
|
-
key: '
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
context: true
|
|
221
|
-
}
|
|
222
|
-
];
|
|
223
|
-
export const logoutResponseFields = [
|
|
192
|
+
key: 'encryptCert'
|
|
193
|
+
},
|
|
194
|
+
// 7.3 签名密钥名称 (KeyName)
|
|
224
195
|
{
|
|
225
|
-
key: '
|
|
226
|
-
localPath: ['LogoutResponse'],
|
|
227
|
-
attributes: ['ID', 'Destination', 'InResponseTo']
|
|
196
|
+
key: 'signingKeyName'
|
|
228
197
|
},
|
|
198
|
+
// 7.4 加密密钥名称 (KeyName)
|
|
229
199
|
{
|
|
230
|
-
key: '
|
|
231
|
-
localPath: ['LogoutResponse', 'Issuer'],
|
|
232
|
-
attributes: []
|
|
200
|
+
key: 'encryptionKeyName'
|
|
233
201
|
},
|
|
202
|
+
// --- 8. 其他扩展属性 (可选) ---
|
|
234
203
|
{
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
204
|
+
// 提取 AttributeConsumingService (如果 IdP 声明了它需要的属性)
|
|
205
|
+
key: 'attributeConsumingService',
|
|
206
|
+
localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'AttributeConsumingService'],
|
|
207
|
+
attributes: ['index', 'isDefault'],
|
|
208
|
+
listMode: true
|
|
239
209
|
}
|
|
240
210
|
];
|
|
211
|
+
// ============================================================================
|
|
212
|
+
// 修正后的 SP 元数据提取字段配置
|
|
213
|
+
// ============================================================================
|
|
214
|
+
export const spMetadataFields = [
|
|
215
|
+
{ key: 'entityID', localPath: ['EntityDescriptor'], attributes: ['entityID'] },
|
|
216
|
+
{ key: 'spSSODescriptor', localPath: ['EntityDescriptor', 'SPSSODescriptor'], attributes: ['protocolSupportEnumeration', 'AuthnRequestsSigned', 'WantAssertionsSigned'] },
|
|
217
|
+
{ key: 'assertionConsumerService', localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AssertionConsumerService'], attributes: ['Binding', 'Location', 'index', 'isDefault'], listMode: true },
|
|
218
|
+
{ key: 'singleLogoutService', localPath: ['EntityDescriptor', 'SPSSODescriptor', 'SingleLogoutService'], attributes: ['Binding', 'Location'], listMode: true },
|
|
219
|
+
{ key: 'artifactResolutionService', localPath: ['EntityDescriptor', 'SPSSODescriptor', 'ArtifactResolutionService'], attributes: ['Binding', 'Location', 'index', 'isDefault'], listMode: true },
|
|
220
|
+
{ key: 'manageNameIDService', localPath: ['EntityDescriptor', 'SPSSODescriptor', 'ManageNameIDService'], attributes: ['Binding', 'Location'], listMode: true },
|
|
221
|
+
{ key: 'nameIDFormat', localPath: ['EntityDescriptor', 'SPSSODescriptor', 'NameIDFormat'], attributes: [] },
|
|
222
|
+
{ key: 'organizationName', localPath: ['EntityDescriptor', 'Organization', 'OrganizationName'], attributes: [] },
|
|
223
|
+
{ key: 'organizationDisplayName', localPath: ['EntityDescriptor', 'Organization', 'OrganizationDisplayName'], attributes: [] },
|
|
224
|
+
{ key: 'organizationURL', localPath: ['EntityDescriptor', 'Organization', 'OrganizationURL'], attributes: [] },
|
|
225
|
+
{ key: 'contactPerson', localPath: ['EntityDescriptor', 'ContactPerson'], attributes: ['contactType'], listMode: true },
|
|
226
|
+
// 特殊字段:触发 extract 内部的硬编码逻辑
|
|
227
|
+
// localPath 和 attributes 在这里不起作用,仅作为占位符
|
|
228
|
+
{ key: 'signingCert' },
|
|
229
|
+
{ key: 'encryptCert' },
|
|
230
|
+
{ key: 'signingKeyName' },
|
|
231
|
+
{ key: 'encryptionKeyName' }
|
|
232
|
+
];
|
|
241
233
|
export function extract(context, fields) {
|
|
242
234
|
const { dom } = getContext();
|
|
243
235
|
const rootDoc = dom.parseFromString(context, 'application/xml');
|
|
244
236
|
return fields.reduce((result, field) => {
|
|
245
|
-
// get essential fields
|
|
246
237
|
const key = field.key;
|
|
247
|
-
|
|
248
|
-
const
|
|
238
|
+
// 安全解构,防止 undefined
|
|
239
|
+
const localPath = field.localPath || [];
|
|
240
|
+
const attributes = field.attributes || [];
|
|
249
241
|
const isEntire = field.context;
|
|
250
242
|
const shortcut = field.shortcut;
|
|
251
|
-
// get optional fields
|
|
252
243
|
const index = field.index;
|
|
253
244
|
const attributePath = field.attributePath;
|
|
254
|
-
|
|
245
|
+
const listMode = field.listMode;
|
|
255
246
|
let targetDoc = rootDoc;
|
|
256
|
-
// if shortcut is used, then replace the doc
|
|
257
|
-
// it's a design for overriding the doc used during runtime
|
|
258
247
|
if (shortcut) {
|
|
259
248
|
targetDoc = dom.parseFromString(shortcut, 'application/xml');
|
|
260
249
|
}
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
.
|
|
250
|
+
// ==========================================================================
|
|
251
|
+
// 【核心修复】特殊处理:证书和 KeyName 提取 (Hardcoded logic)
|
|
252
|
+
// 不再硬编码 IDPSSODescriptor 或 SPSSODescriptor,而是全局搜索 @use 属性
|
|
253
|
+
// ==========================================================================
|
|
254
|
+
if (key === 'signingCert' || key === 'encryptCert' || key === 'signingKeyName' || key === 'encryptionKeyName') {
|
|
255
|
+
const isSigning = key.startsWith('signing');
|
|
256
|
+
const useType = isSigning ? 'signing' : 'encryption';
|
|
257
|
+
const isKeyName = key.endsWith('KeyName');
|
|
258
|
+
// 通用 XPath:查找任意层级下符合 @use 条件的 KeyDescriptor
|
|
259
|
+
const kdXPath = `//*[local-name(.)='KeyDescriptor' and @use='${useType}']`;
|
|
260
|
+
let fullXPath = '';
|
|
261
|
+
if (isKeyName) {
|
|
262
|
+
// 提取 KeyName 文本
|
|
263
|
+
fullXPath = `${kdXPath}/*[local-name(.)='KeyInfo']/*[local-name(.)='KeyName']/text()`;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// 提取 X509Certificate 文本
|
|
267
|
+
fullXPath = `${kdXPath}/*[local-name(.)='KeyInfo']/*[local-name(.)='X509Data']/*[local-name(.)='X509Certificate']/text()`;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
// @ts-ignore
|
|
271
|
+
const nodes = select(fullXPath, targetDoc);
|
|
272
|
+
if (isKeyName) {
|
|
273
|
+
const keyNames = nodes.map((n) => n.nodeValue).filter(notEmpty);
|
|
274
|
+
return {
|
|
275
|
+
...result,
|
|
276
|
+
[key]: keyNames.length > 0 ? keyNames[0] : null
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
const certs = nodes.map((n) => {
|
|
281
|
+
const val = n.nodeValue || n.value;
|
|
282
|
+
return val ? val.replace(/\r\n|\r|\n/g, '') : null;
|
|
283
|
+
}).filter(notEmpty);
|
|
284
|
+
return {
|
|
285
|
+
...result,
|
|
286
|
+
[key]: certs.length > 0 ? certs[0] : null
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
console.error(`Error extracting ${key}:`, e);
|
|
292
|
+
return { ...result, [key]: null };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// 特殊 case: 多路径 (原有逻辑)
|
|
296
|
+
if (Array.isArray(localPath) && localPath.length > 0 && Array.isArray(localPath[0])) {
|
|
297
|
+
const multiXPaths = localPath.map(path => `${buildAbsoluteXPath(path)}/text()`).join(' | ');
|
|
298
|
+
// @ts-ignore
|
|
299
|
+
const nodes = select(multiXPaths, targetDoc);
|
|
300
|
+
return {
|
|
301
|
+
...result,
|
|
302
|
+
[key]: uniq(nodes.map((n) => n.nodeValue).filter(notEmpty))
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
// 此时 localPath 必然是 string[]
|
|
306
|
+
const currentLocalPath = localPath;
|
|
307
|
+
// 如果 localPath 为空数组(如特殊字段未定义 path),且未命中上面的特殊逻辑,则跳过
|
|
308
|
+
if (currentLocalPath.length === 0 && !isEntire) {
|
|
309
|
+
// 对于没有 path 且不是特殊处理的字段,返回 null 或跳过
|
|
310
|
+
return { ...result, [key]: null };
|
|
311
|
+
}
|
|
312
|
+
const baseXPath = buildAbsoluteXPath(currentLocalPath);
|
|
313
|
+
// --- 新增:列表模式处理 (用于 SSO Service, ACS 等) ---
|
|
314
|
+
if (listMode && attributes.length > 0) {
|
|
315
|
+
// @ts-ignore
|
|
316
|
+
const nodes = select(baseXPath, targetDoc);
|
|
317
|
+
const resultList = nodes.map((node) => {
|
|
318
|
+
const attrResult = {};
|
|
319
|
+
attributes.forEach(attr => {
|
|
320
|
+
if (node.getAttribute) {
|
|
321
|
+
const val = node.getAttribute(attr);
|
|
322
|
+
if (val) {
|
|
323
|
+
attrResult[camelCase(attr, { locale: 'en-us' })] = val;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
return attrResult;
|
|
328
|
+
});
|
|
279
329
|
return {
|
|
280
330
|
...result,
|
|
281
|
-
|
|
282
|
-
[key]: uniq(select(multiXPaths, targetDoc).map((n) => n.nodeValue).filter(notEmpty))
|
|
331
|
+
[key]: resultList
|
|
283
332
|
};
|
|
284
333
|
}
|
|
285
|
-
// eo special case: multiple path
|
|
286
|
-
const baseXPath = buildAbsoluteXPath(localPath);
|
|
287
334
|
const attributeXPath = buildAttributeXPath(attributes);
|
|
288
|
-
//
|
|
289
|
-
/*
|
|
290
|
-
{
|
|
291
|
-
key: 'attributes',
|
|
292
|
-
localPath: ['Response', 'Assertion', 'AttributeStatement', 'Attribute'],
|
|
293
|
-
index: ['Name'],
|
|
294
|
-
attributePath: ['AttributeValue'],
|
|
295
|
-
attributes: []
|
|
296
|
-
}
|
|
297
|
-
*/
|
|
335
|
+
// 特殊 case: 属性聚合 (原有逻辑 - Attributes with index)
|
|
298
336
|
if (index && attributePath) {
|
|
299
|
-
// find the index in localpath
|
|
300
337
|
const indexPath = buildAttributeXPath(index);
|
|
301
338
|
const fullLocalXPath = `${baseXPath}${indexPath}`;
|
|
302
|
-
// @ts-
|
|
339
|
+
// @ts-ignore
|
|
303
340
|
const parentNodes = select(baseXPath, targetDoc);
|
|
304
|
-
//
|
|
305
|
-
// @ts-expect-error misssing Node properties are not needed
|
|
341
|
+
// @ts-ignore
|
|
306
342
|
const parentAttributes = select(fullLocalXPath, targetDoc).map((n) => n.value);
|
|
307
|
-
|
|
308
|
-
const childXPath = buildAbsoluteXPath([last(localPath)].concat(attributePath));
|
|
343
|
+
const childXPath = buildAbsoluteXPath([last(currentLocalPath)].concat(attributePath));
|
|
309
344
|
const childAttributeXPath = buildAttributeXPath(attributes);
|
|
310
345
|
const fullChildXPath = `${childXPath}${childAttributeXPath}`;
|
|
311
|
-
|
|
312
|
-
const childAttributes = parentNodes.map(node => {
|
|
346
|
+
const childAttributes = parentNodes.map((node) => {
|
|
313
347
|
const nodeDoc = dom.parseFromString(node.toString(), 'application/xml');
|
|
314
348
|
if (attributes.length === 0) {
|
|
315
349
|
// @ts-ignore
|
|
316
350
|
const childValues = select(fullChildXPath, nodeDoc).map((n) => n.nodeValue);
|
|
317
|
-
|
|
318
|
-
return childValues[0];
|
|
319
|
-
}
|
|
320
|
-
return childValues;
|
|
351
|
+
return childValues.length === 1 ? childValues[0] : childValues;
|
|
321
352
|
}
|
|
322
353
|
if (attributes.length > 0) {
|
|
323
354
|
// @ts-ignore
|
|
324
355
|
const childValues = select(fullChildXPath, nodeDoc).map((n) => n.value);
|
|
325
|
-
|
|
326
|
-
return childValues[0];
|
|
327
|
-
}
|
|
328
|
-
return childValues;
|
|
356
|
+
return childValues.length === 1 ? childValues[0] : childValues;
|
|
329
357
|
}
|
|
330
358
|
return null;
|
|
331
359
|
});
|
|
332
|
-
// aggregation
|
|
333
360
|
const obj = zipObject(parentAttributes, childAttributes, false);
|
|
334
|
-
return {
|
|
335
|
-
...result,
|
|
336
|
-
[key]: obj
|
|
337
|
-
};
|
|
361
|
+
return { ...result, [key]: obj };
|
|
338
362
|
}
|
|
339
|
-
// case:
|
|
340
|
-
/*
|
|
341
|
-
{
|
|
342
|
-
key: 'signature',
|
|
343
|
-
localPath: ['AuthnRequest', 'Signature'],
|
|
344
|
-
attributes: [],
|
|
345
|
-
context: true
|
|
346
|
-
}
|
|
347
|
-
*/
|
|
363
|
+
// 特殊 case: 获取整个节点内容 (原有逻辑)
|
|
348
364
|
if (isEntire) {
|
|
349
|
-
// @ts-
|
|
365
|
+
// @ts-ignore
|
|
350
366
|
const node = select(baseXPath, targetDoc);
|
|
351
367
|
let value = null;
|
|
352
368
|
if (node.length === 1) {
|
|
353
369
|
value = node[0].toString();
|
|
354
370
|
}
|
|
355
371
|
if (node.length > 1) {
|
|
356
|
-
value = node.map(n => n.toString());
|
|
372
|
+
value = node.map((n) => n.toString());
|
|
357
373
|
}
|
|
358
|
-
return {
|
|
359
|
-
...result,
|
|
360
|
-
[key]: value
|
|
361
|
-
};
|
|
374
|
+
return { ...result, [key]: value };
|
|
362
375
|
}
|
|
363
|
-
// case:
|
|
364
|
-
|
|
365
|
-
{
|
|
366
|
-
key: 'nameIDPolicy',
|
|
367
|
-
localPath: ['AuthnRequest', 'NameIDPolicy'],
|
|
368
|
-
attributes: ['Format', 'AllowCreate']
|
|
369
|
-
}
|
|
370
|
-
*/
|
|
371
|
-
if (attributes.length > 1) {
|
|
376
|
+
// 特殊 case: 多属性对象 (原有逻辑,非 listMode)
|
|
377
|
+
if (attributes.length > 1 && !listMode) {
|
|
372
378
|
// @ts-ignore
|
|
373
|
-
const baseNode = select(baseXPath, targetDoc).map(n => n.toString());
|
|
374
|
-
const childXPath = `${buildAbsoluteXPath([last(
|
|
375
|
-
const attributeValues = baseNode.map((
|
|
379
|
+
const baseNode = select(baseXPath, targetDoc).map((n) => n.toString());
|
|
380
|
+
const childXPath = `${buildAbsoluteXPath([last(currentLocalPath)])}${attributeXPath}`;
|
|
381
|
+
const attributeValues = baseNode.map((nodeStr) => {
|
|
376
382
|
// @ts-ignore
|
|
377
|
-
const nodeDoc = dom.parseFromString(
|
|
383
|
+
const nodeDoc = dom.parseFromString(nodeStr, 'application/xml');
|
|
378
384
|
// @ts-ignore
|
|
379
385
|
const values = select(childXPath, nodeDoc).reduce((r, n) => {
|
|
380
386
|
r[camelCase(n.name, { locale: 'en-us' })] = n.value;
|
|
@@ -387,49 +393,35 @@ export function extract(context, fields) {
|
|
|
387
393
|
[key]: attributeValues.length === 1 ? attributeValues[0] : attributeValues
|
|
388
394
|
};
|
|
389
395
|
}
|
|
390
|
-
// case:
|
|
391
|
-
|
|
392
|
-
{
|
|
393
|
-
key: 'statusCode',
|
|
394
|
-
localPath: ['Response', 'Status', 'StatusCode'],
|
|
395
|
-
attributes: ['Value'],
|
|
396
|
-
}
|
|
397
|
-
*/
|
|
398
|
-
if (attributes.length === 1) {
|
|
396
|
+
// 特殊 case: 单个属性 (原有逻辑)
|
|
397
|
+
if (attributes.length === 1 && !listMode) {
|
|
399
398
|
const fullPath = `${baseXPath}${attributeXPath}`;
|
|
400
399
|
// @ts-ignore
|
|
401
400
|
const attributeValues = select(fullPath, targetDoc).map((n) => n.value);
|
|
402
|
-
return {
|
|
403
|
-
...result,
|
|
404
|
-
[key]: attributeValues[0]
|
|
405
|
-
};
|
|
401
|
+
return { ...result, [key]: attributeValues[0] };
|
|
406
402
|
}
|
|
407
|
-
// case:
|
|
408
|
-
|
|
409
|
-
{
|
|
410
|
-
key: 'issuer',
|
|
411
|
-
localPath: ['AuthnRequest', 'Issuer'],
|
|
412
|
-
attributes: []
|
|
413
|
-
}
|
|
414
|
-
*/
|
|
415
|
-
if (attributes.length === 0) {
|
|
403
|
+
// 特殊 case: 无属性/文本内容 (原有逻辑)
|
|
404
|
+
if (attributes.length === 0 && !listMode) {
|
|
416
405
|
let attributeValue = null;
|
|
417
|
-
// @ts-
|
|
406
|
+
// @ts-ignore
|
|
418
407
|
const node = select(baseXPath, targetDoc);
|
|
419
408
|
if (node.length === 1) {
|
|
420
409
|
const fullPath = `string(${baseXPath}${attributeXPath})`;
|
|
421
|
-
// @ts-
|
|
410
|
+
// @ts-ignore
|
|
422
411
|
attributeValue = select(fullPath, targetDoc);
|
|
423
412
|
}
|
|
424
413
|
if (node.length > 1) {
|
|
425
414
|
attributeValue = node.filter((n) => n.firstChild)
|
|
426
|
-
.map((n) => n.firstChild
|
|
415
|
+
.map((n) => n.firstChild?.nodeValue);
|
|
427
416
|
}
|
|
428
|
-
return {
|
|
429
|
-
...result,
|
|
430
|
-
[key]: attributeValue
|
|
431
|
-
};
|
|
417
|
+
return { ...result, [key]: attributeValue };
|
|
432
418
|
}
|
|
433
419
|
return result;
|
|
434
420
|
}, {});
|
|
435
421
|
}
|
|
422
|
+
export function extractIdp(context) {
|
|
423
|
+
return extract(context, idpMetadataFields);
|
|
424
|
+
}
|
|
425
|
+
export function extractSp(context) {
|
|
426
|
+
return extract(context, spMetadataFields);
|
|
427
|
+
}
|