samlesa 3.4.1 → 3.4.3
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-artifact.js +101 -129
- package/build/src/entity-idp.js +15 -1
- package/build/src/extractor.js +46 -19
- package/build/src/libsaml.js +27 -50
- package/build/src/libsamlSoap.js +10 -3
- package/build/src/metadata.js +2 -2
- package/build/src/schemaValidator.js +73 -56
- package/build/src/urn.js +17 -1
- package/build/src/utility.js +0 -2
- package/package.json +75 -76
- package/types/src/binding-artifact.d.ts +3 -11
- package/types/src/binding-artifact.d.ts.map +1 -1
- package/types/src/entity-idp.d.ts.map +1 -1
- package/types/src/entity-sp.d.ts +1 -1
- package/types/src/extractor.d.ts +2 -1
- package/types/src/extractor.d.ts.map +1 -1
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/libsaml.d.ts +1 -14
- package/types/src/libsaml.d.ts.map +1 -1
- package/types/src/libsamlSoap.d.ts +1 -1
- package/types/src/libsamlSoap.d.ts.map +1 -1
- package/types/src/schemaValidator.d.ts +18 -1
- package/types/src/schemaValidator.d.ts.map +1 -1
- package/types/src/urn.d.ts +9 -1
- package/types/src/urn.d.ts.map +1 -1
- package/types/src/utility.d.ts.map +1 -1
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { checkStatus } from "./flow.js";
|
|
7
7
|
import { ParserType, StatusCode, wording } from './urn.js';
|
|
8
|
+
import * as crypto from "node:crypto";
|
|
8
9
|
import libsaml from './libsaml.js';
|
|
9
10
|
import libsamlSoap from './libsamlSoap.js';
|
|
10
11
|
import utility, { get } from './utility.js';
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
12
13
|
import { randomUUID } from 'node:crypto';
|
|
14
|
+
import postBinding from './binding-post.js';
|
|
13
15
|
import { artifactResolveFields, extract, loginRequestFields, loginResponseFields, logoutRequestFields, logoutResponseFields } from "./extractor.js";
|
|
14
16
|
import { verifyTime } from "./validator.js";
|
|
15
17
|
import { sendArtifactResolve } from "./soap.js";
|
|
@@ -137,141 +139,111 @@ function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
|
|
|
137
139
|
throw new Error('ERR_GENERATE_POST_LOGIN_REQUEST_MISSING_METADATA');
|
|
138
140
|
}
|
|
139
141
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* @param
|
|
143
|
-
* @param
|
|
144
|
-
* @
|
|
145
|
-
* @param {boolean} encryptThenSign whether or not to encrypt then sign first (if signing). Defaults to sign-then-encrypt
|
|
146
|
-
* @param AttributeStatement
|
|
142
|
+
* Generates a SAML 2.0 compliant Artifact ID.
|
|
143
|
+
* Format: [4-byte source ID][2-byte endpoint index][20-byte sequence value]
|
|
144
|
+
* @param issuerId The entity ID of the issuing party (IdP).
|
|
145
|
+
* @param endpointIndex The index of the destination endpoint (default is '0004' for Artifact Resolution Service).
|
|
146
|
+
* @returns The Base64 encoded Artifact ID string.
|
|
147
147
|
*/
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
/**
|
|
149
|
+
* 生成符合 SAML 2.0 规范的 Artifact
|
|
150
|
+
* 结构: [TypeCode: 2 bytes] + [EndpointIndex: 2 bytes] + [SourceID: 20 bytes] + [MessageHandle: 20 bytes]
|
|
151
|
+
*/
|
|
152
|
+
function generateArtifactId(issuerId, endpointIndex = 1) {
|
|
153
|
+
// 1. SourceID: 20 bytes SHA-1
|
|
154
|
+
const issuerHash = crypto.createHash('sha1').update(issuerId).digest();
|
|
155
|
+
const sourceId = issuerHash.subarray(0, 20); // 必须 20 字节
|
|
156
|
+
// 2. TypeCode: 0x0004
|
|
157
|
+
const typeCode = Buffer.from([0x00, 0x04]);
|
|
158
|
+
// 3. EndpointIndex: 2 bytes Big Endian
|
|
159
|
+
const indexBuffer = Buffer.alloc(2);
|
|
160
|
+
indexBuffer.writeUInt16BE(endpointIndex, 0);
|
|
161
|
+
// 4. MessageHandle: 20 random bytes
|
|
162
|
+
const messageHandle = crypto.randomBytes(20);
|
|
163
|
+
// 5. Concat: 2 + 2 + 20 + 20 = 44 bytes
|
|
164
|
+
const artifactBytes = Buffer.concat([typeCode, indexBuffer, sourceId, messageHandle]);
|
|
165
|
+
// 6. Base64
|
|
166
|
+
return artifactBytes.toString('base64');
|
|
167
|
+
}
|
|
168
|
+
// Initial response that sends only the artifact ID
|
|
169
|
+
async function soapLoginResponse(params) {
|
|
170
|
+
const { entity } = params;
|
|
152
171
|
const metadata = {
|
|
153
172
|
idp: entity.idp.entityMeta,
|
|
154
173
|
sp: entity.sp.entityMeta,
|
|
155
174
|
};
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (metadata && metadata.idp && metadata.sp) {
|
|
159
|
-
const base = metadata.sp.getAssertionConsumerService(binding.post);
|
|
160
|
-
let rawSamlResponse;
|
|
161
|
-
const nowTime = new Date();
|
|
162
|
-
const spEntityID = metadata.sp.getEntityID();
|
|
163
|
-
const oneMinutesLaterTime = new Date(nowTime.getTime());
|
|
164
|
-
oneMinutesLaterTime.setMinutes(oneMinutesLaterTime.getMinutes() + 5);
|
|
165
|
-
const OneMinutesLater = oneMinutesLaterTime.toISOString();
|
|
166
|
-
const now = nowTime.toISOString();
|
|
167
|
-
const acl = metadata.sp.getAssertionConsumerService(binding.post);
|
|
168
|
-
const sessionIndex = 'session' + idpSetting.generateID(); // 这个是当前系统的会话索引,用于单点注销
|
|
169
|
-
const tenHoursLaterTime = new Date(nowTime.getTime());
|
|
170
|
-
tenHoursLaterTime.setHours(tenHoursLaterTime.getHours() + 10);
|
|
171
|
-
const tenHoursLater = tenHoursLaterTime.toISOString();
|
|
172
|
-
const tvalue = {
|
|
173
|
-
ID: id,
|
|
174
|
-
AssertionID: idpSetting.generateID(),
|
|
175
|
-
Destination: base,
|
|
176
|
-
Audience: spEntityID,
|
|
177
|
-
EntityID: spEntityID,
|
|
178
|
-
SubjectRecipient: acl,
|
|
179
|
-
Issuer: metadata.idp.getEntityID(),
|
|
180
|
-
IssueInstant: now,
|
|
181
|
-
AssertionConsumerServiceURL: acl,
|
|
182
|
-
StatusCode: StatusCode.Success,
|
|
183
|
-
// can be customized
|
|
184
|
-
ConditionsNotBefore: now,
|
|
185
|
-
ConditionsNotOnOrAfter: OneMinutesLater,
|
|
186
|
-
SubjectConfirmationDataNotOnOrAfter: OneMinutesLater,
|
|
187
|
-
NameIDFormat: selectedNameIDFormat,
|
|
188
|
-
NameID: user?.NameID || '',
|
|
189
|
-
InResponseTo: get(requestInfo, 'extract.request.id', ''),
|
|
190
|
-
AuthnStatement: `<saml:AuthnStatement AuthnInstant="${now}" SessionNotOnOrAfter="${tenHoursLater}" SessionIndex="${sessionIndex}"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement>`,
|
|
191
|
-
AttributeStatement: libsaml.attributeStatementBuilder(AttributeStatement),
|
|
192
|
-
};
|
|
193
|
-
if (idpSetting.loginResponseTemplate && customTagReplacement) {
|
|
194
|
-
const template = customTagReplacement(idpSetting.loginResponseTemplate.context);
|
|
195
|
-
rawSamlResponse = get(template, 'context', null);
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
if (requestInfo !== null) {
|
|
199
|
-
tvalue.InResponseTo = requestInfo?.extract?.request?.id ?? '';
|
|
200
|
-
}
|
|
201
|
-
rawSamlResponse = libsaml.replaceTagsByValue(libsaml.defaultLoginResponseTemplate.context, tvalue);
|
|
202
|
-
}
|
|
203
|
-
const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm } = idpSetting;
|
|
204
|
-
const config = {
|
|
205
|
-
privateKey,
|
|
206
|
-
privateKeyPass,
|
|
207
|
-
signatureAlgorithm,
|
|
208
|
-
signingCert: metadata.idp.getX509Certificate('signing'),
|
|
209
|
-
isBase64Output: false,
|
|
210
|
-
};
|
|
211
|
-
// step: sign assertion ? -> encrypted ? -> sign message ?
|
|
212
|
-
if (metadata.sp.isWantAssertionsSigned()) {
|
|
213
|
-
rawSamlResponse = libsaml.constructSAMLSignature({
|
|
214
|
-
...config,
|
|
215
|
-
rawSamlMessage: rawSamlResponse,
|
|
216
|
-
transformationAlgorithms: spSetting.transformationAlgorithms,
|
|
217
|
-
referenceTagXPath: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']",
|
|
218
|
-
signatureConfig: {
|
|
219
|
-
prefix: 'ds',
|
|
220
|
-
location: {
|
|
221
|
-
reference: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']/*[local-name(.)='Issuer']",
|
|
222
|
-
action: 'after'
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
// console.debug('after assertion signed', rawSamlResponse);
|
|
228
|
-
// SAML response must be signed sign message first, then encrypt
|
|
229
|
-
if (!encryptThenSign && (spSetting.wantMessageSigned || !metadata.sp.isWantAssertionsSigned())) {
|
|
230
|
-
// console.debug('sign then encrypt and sign entire message');
|
|
231
|
-
// @ts-ignore
|
|
232
|
-
rawSamlResponse = libsaml.constructSAMLSignature({
|
|
233
|
-
...config,
|
|
234
|
-
rawSamlMessage: rawSamlResponse,
|
|
235
|
-
isMessageSigned: true,
|
|
236
|
-
transformationAlgorithms: spSetting.transformationAlgorithms,
|
|
237
|
-
signatureConfig: spSetting.signatureConfig || {
|
|
238
|
-
prefix: 'ds',
|
|
239
|
-
location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Issuer']", action: 'after' },
|
|
240
|
-
},
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
// console.debug('after message signed', rawSamlResponse);
|
|
244
|
-
if (idpSetting.isAssertionEncrypted) {
|
|
245
|
-
// console.debug('idp is configured to do encryption');
|
|
246
|
-
const context = await libsaml.encryptAssertion(entity.idp, entity.sp, rawSamlResponse);
|
|
247
|
-
if (encryptThenSign) {
|
|
248
|
-
//need to decode it
|
|
249
|
-
rawSamlResponse = utility.base64Decode(context);
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
return Promise.resolve({ id, context });
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
//sign after encrypting
|
|
256
|
-
if (encryptThenSign && (spSetting.wantMessageSigned || !metadata.sp.isWantAssertionsSigned())) {
|
|
257
|
-
// @ts-ignore
|
|
258
|
-
rawSamlResponse = libsaml.constructSAMLSignature({
|
|
259
|
-
...config,
|
|
260
|
-
rawSamlMessage: rawSamlResponse,
|
|
261
|
-
isMessageSigned: true,
|
|
262
|
-
transformationAlgorithms: spSetting.transformationAlgorithms,
|
|
263
|
-
signatureConfig: spSetting.signatureConfig || {
|
|
264
|
-
prefix: 'ds',
|
|
265
|
-
location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Issuer']", action: 'after' },
|
|
266
|
-
},
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
return Promise.resolve({
|
|
270
|
-
id,
|
|
271
|
-
context: utility.base64Encode(rawSamlResponse),
|
|
272
|
-
});
|
|
175
|
+
if (!metadata.idp || !metadata.sp) {
|
|
176
|
+
throw new Error('ERR_GENERATE_ARTIFACT_MISSING_METADATA');
|
|
273
177
|
}
|
|
274
|
-
|
|
178
|
+
// 1. Generate the base SAML Response (Base64 encoded)
|
|
179
|
+
const samlResponseResult = await postBinding.base64LoginResponse(params);
|
|
180
|
+
const samlResponseXml = utility.base64Decode(samlResponseResult.context);
|
|
181
|
+
console.log(samlResponseXml);
|
|
182
|
+
console.log("我日你妈==");
|
|
183
|
+
// 2. Generate the SAML 2.0 Artifact ID
|
|
184
|
+
const artifactId = generateArtifactId(metadata.idp.getEntityID());
|
|
185
|
+
// 3. Cache the SAML Response XML using the artifactId as the key
|
|
186
|
+
// TODO: Implement your Redis caching logic here
|
|
187
|
+
// Example:
|
|
188
|
+
// const redisExpirySeconds = 300; // e.g., expire after 5 minutes
|
|
189
|
+
// await redis.setex(`artifact_cache:${artifactId}`, redisExpirySeconds, samlResponseXml);
|
|
190
|
+
// --- INSERT YOUR REDIS LOGIC HERE ---
|
|
191
|
+
// Example: await cacheInRedis(artifactId, samlResponseXml);
|
|
192
|
+
// 4. Prepare config for SOAP signing (reusing IDP settings)
|
|
193
|
+
const idpSetting = entity.idp.entitySetting;
|
|
194
|
+
const spSetting = entity.sp.entitySetting;
|
|
195
|
+
const config = {
|
|
196
|
+
privateKey: idpSetting.privateKey,
|
|
197
|
+
privateKeyPass: idpSetting.privateKeyPass,
|
|
198
|
+
signatureAlgorithm: idpSetting.requestSignatureAlgorithm,
|
|
199
|
+
signingCert: metadata.idp.getX509Certificate('signing'),
|
|
200
|
+
isBase64Output: false,
|
|
201
|
+
};
|
|
202
|
+
// 5. Construct the SOAP Envelope containing the Artifact ID
|
|
203
|
+
// This is CORRECT - in the initial response we only send the artifact ID
|
|
204
|
+
// The actual SAML response will be retrieved by the SP using the artifact resolution service
|
|
205
|
+
const soapBodyContent = `<samlp:ArtifactResponse
|
|
206
|
+
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
|
207
|
+
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
|
208
|
+
ID="${samlResponseResult.id}_artifact_resp"
|
|
209
|
+
InResponseTo="${params.requestInfo?.extract?.request?.id || ''}"
|
|
210
|
+
Version="2.0"
|
|
211
|
+
IssueInstant="${new Date().toISOString()}">
|
|
212
|
+
<saml2:Issuer>${metadata.idp.getEntityID()}</saml2:Issuer>
|
|
213
|
+
<samlp:Status>
|
|
214
|
+
<samlp:StatusCode Value="${StatusCode.Success}"/>
|
|
215
|
+
</samlp:Status>
|
|
216
|
+
${samlResponseXml}
|
|
217
|
+
</samlp:ArtifactResponse>`;
|
|
218
|
+
const soapEnvelope = `
|
|
219
|
+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
|
220
|
+
<soap:Header/>
|
|
221
|
+
<soap:Body>${soapBodyContent}</soap:Body>
|
|
222
|
+
</soap:Envelope>`;
|
|
223
|
+
// 6. Sign the SOAP Envelope
|
|
224
|
+
// Reference the Body element for signature
|
|
225
|
+
// @ts-ignore
|
|
226
|
+
// @ts-ignore
|
|
227
|
+
const signedSoapEnvelope = libsaml.constructSAMLSignature({
|
|
228
|
+
...config,
|
|
229
|
+
rawSamlMessage: soapEnvelope,
|
|
230
|
+
transformationAlgorithms: spSetting.transformationAlgorithms,
|
|
231
|
+
isMessageSigned: true,
|
|
232
|
+
referenceTagXPath: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']",
|
|
233
|
+
signatureConfig: {
|
|
234
|
+
prefix: 'ds',
|
|
235
|
+
location: {
|
|
236
|
+
// Place signature inside the ArtifactResponse element, before the Status element
|
|
237
|
+
reference: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']/*[local-name(.)='Status']",
|
|
238
|
+
action: 'before'
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
// 7. Return the Artifact ID and the Base64 encoded signed SOAP message
|
|
243
|
+
return {
|
|
244
|
+
id: artifactId, // This is the key you'll need for resolving the artifact later
|
|
245
|
+
context: utility.base64Encode(signedSoapEnvelope), // The SOAP message to send back
|
|
246
|
+
};
|
|
275
247
|
}
|
|
276
248
|
async function parseLoginRequestResolve(params) {
|
|
277
249
|
let { idp, sp, xml, } = params;
|
package/build/src/entity-idp.js
CHANGED
|
@@ -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,19 @@ export class IdentityProvider extends Entity {
|
|
|
66
67
|
idp: this,
|
|
67
68
|
sp,
|
|
68
69
|
}, user, relayState, customTagReplacement, AttributeStatement);
|
|
70
|
+
case namespace.binding.artifact:
|
|
71
|
+
return artifactBinding.soapLoginResponse({
|
|
72
|
+
requestInfo,
|
|
73
|
+
entity: {
|
|
74
|
+
idp: this,
|
|
75
|
+
sp,
|
|
76
|
+
},
|
|
77
|
+
user,
|
|
78
|
+
customTagReplacement,
|
|
79
|
+
encryptThenSign,
|
|
80
|
+
AttributeStatement,
|
|
81
|
+
idpInit,
|
|
82
|
+
});
|
|
69
83
|
default:
|
|
70
84
|
throw new Error('ERR_CREATE_RESPONSE_UNDEFINED_BINDING');
|
|
71
85
|
}
|
package/build/src/extractor.js
CHANGED
|
@@ -2,6 +2,13 @@ import { select } from 'xpath';
|
|
|
2
2
|
import { uniq, last, zipObject, notEmpty } from './utility.js'; // 假设这些工具函数存在
|
|
3
3
|
import { getContext } from './api.js'; // 假设这个API存在
|
|
4
4
|
import camelCase from 'camelcase';
|
|
5
|
+
function toNodeArray(result) {
|
|
6
|
+
if (Array.isArray(result))
|
|
7
|
+
return result;
|
|
8
|
+
if (result != null && typeof result === 'object' && 'nodeType' in result)
|
|
9
|
+
return [result];
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
5
12
|
function buildAbsoluteXPath(paths) {
|
|
6
13
|
if (!paths || paths.length === 0)
|
|
7
14
|
return '';
|
|
@@ -110,10 +117,27 @@ export const loginRequestFields = [
|
|
|
110
117
|
{ key: 'signature', localPath: ['AuthnRequest', 'Signature'], attributes: [], context: true }
|
|
111
118
|
];
|
|
112
119
|
export const artifactResolveFields = [
|
|
113
|
-
{
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
},
|
|
117
141
|
];
|
|
118
142
|
export const artifactResponseFields = [
|
|
119
143
|
{ key: 'request', localPath: ['Envelope', 'Body', 'ArtifactResolve'], attributes: ['ID', 'IssueInstant', 'Version'] },
|
|
@@ -693,7 +717,7 @@ export function extract(context, fields) {
|
|
|
693
717
|
}
|
|
694
718
|
try {
|
|
695
719
|
// @ts-ignore
|
|
696
|
-
const nodes = select(fullXPath, targetDoc);
|
|
720
|
+
const nodes = toNodeArray(select(fullXPath, targetDoc));
|
|
697
721
|
if (isKeyName) {
|
|
698
722
|
const keyNames = nodes.map((n) => n.nodeValue).filter(notEmpty);
|
|
699
723
|
return {
|
|
@@ -721,7 +745,7 @@ export function extract(context, fields) {
|
|
|
721
745
|
if (Array.isArray(localPath) && localPath.length > 0 && Array.isArray(localPath[0])) {
|
|
722
746
|
const multiXPaths = localPath.map(path => `${buildAbsoluteXPath(path)}/text()`).join(' | ');
|
|
723
747
|
// @ts-ignore
|
|
724
|
-
const nodes = select(multiXPaths, targetDoc);
|
|
748
|
+
const nodes = toNodeArray(select(multiXPaths, targetDoc));
|
|
725
749
|
return {
|
|
726
750
|
...result,
|
|
727
751
|
[key]: uniq(nodes.map((n) => n.nodeValue).filter(notEmpty))
|
|
@@ -738,7 +762,7 @@ export function extract(context, fields) {
|
|
|
738
762
|
// --- 新增:列表模式处理 (用于 SSO Service, ACS 等) ---
|
|
739
763
|
if (listMode && attributes.length > 0) {
|
|
740
764
|
// @ts-ignore
|
|
741
|
-
const nodes = select(baseXPath, targetDoc);
|
|
765
|
+
const nodes = toNodeArray(select(baseXPath, targetDoc));
|
|
742
766
|
const resultList = nodes.map((node) => {
|
|
743
767
|
const attrResult = {};
|
|
744
768
|
attributes.forEach(attr => {
|
|
@@ -762,7 +786,7 @@ export function extract(context, fields) {
|
|
|
762
786
|
const indexPath = buildAttributeXPath(index);
|
|
763
787
|
const fullLocalXPath = `${baseXPath}${indexPath}`;
|
|
764
788
|
// @ts-ignore
|
|
765
|
-
const parentNodes = select(baseXPath, targetDoc);
|
|
789
|
+
const parentNodes = toNodeArray(select(baseXPath, targetDoc));
|
|
766
790
|
// @ts-ignore
|
|
767
791
|
const parentAttributes = select(fullLocalXPath, targetDoc).map((n) => n.value);
|
|
768
792
|
const childXPath = buildAbsoluteXPath([last(currentLocalPath)].concat(attributePath));
|
|
@@ -788,7 +812,7 @@ export function extract(context, fields) {
|
|
|
788
812
|
// 特殊 case: 获取整个节点内容 (原有逻辑)
|
|
789
813
|
if (isEntire) {
|
|
790
814
|
// @ts-ignore
|
|
791
|
-
const node = select(baseXPath, targetDoc);
|
|
815
|
+
const node = toNodeArray(select(baseXPath, targetDoc));
|
|
792
816
|
let value = null;
|
|
793
817
|
if (node.length === 1) {
|
|
794
818
|
value = node[0].toString();
|
|
@@ -829,7 +853,7 @@ export function extract(context, fields) {
|
|
|
829
853
|
if (attributes.length === 0 && !listMode) {
|
|
830
854
|
let attributeValue = null;
|
|
831
855
|
// @ts-ignore
|
|
832
|
-
const node = select(baseXPath, targetDoc);
|
|
856
|
+
const node = toNodeArray(select(baseXPath, targetDoc));
|
|
833
857
|
if (node.length === 1) {
|
|
834
858
|
const fullPath = `string(${baseXPath}${attributeXPath})`;
|
|
835
859
|
// @ts-ignore
|
|
@@ -1258,7 +1282,7 @@ export function extractSpToll(context, fields) {
|
|
|
1258
1282
|
}
|
|
1259
1283
|
try {
|
|
1260
1284
|
// @ts-ignore
|
|
1261
|
-
const nodes = select(fullXPath, targetDoc);
|
|
1285
|
+
const nodes = toNodeArray(select(fullXPath, targetDoc));
|
|
1262
1286
|
if (isKeyName) {
|
|
1263
1287
|
const keyNames = nodes.map((n) => n.nodeValue).filter(notEmpty);
|
|
1264
1288
|
return { ...result, [key]: keyNames.length > 0 ? keyNames[0] : null };
|
|
@@ -1280,7 +1304,7 @@ export function extractSpToll(context, fields) {
|
|
|
1280
1304
|
const multiXPaths = localPath.map(path => `${buildAbsoluteXPath(path)}/text()`).join(' | ');
|
|
1281
1305
|
try {
|
|
1282
1306
|
// @ts-ignore
|
|
1283
|
-
const nodes = select(multiXPaths, targetDoc);
|
|
1307
|
+
const nodes = toNodeArray(select(multiXPaths, targetDoc));
|
|
1284
1308
|
return { ...result, [key]: uniq(nodes.map((n) => n.nodeValue).filter(notEmpty)) };
|
|
1285
1309
|
}
|
|
1286
1310
|
catch (e) {
|
|
@@ -1296,7 +1320,7 @@ export function extractSpToll(context, fields) {
|
|
|
1296
1320
|
if (listMode) {
|
|
1297
1321
|
try {
|
|
1298
1322
|
// @ts-ignore
|
|
1299
|
-
const nodes = select(baseXPath, targetDoc);
|
|
1323
|
+
const nodes = toNodeArray(select(baseXPath, targetDoc));
|
|
1300
1324
|
if (parseCallback) {
|
|
1301
1325
|
// 使用自定义回调函数处理列表
|
|
1302
1326
|
return { ...result, [key]: parseCallback(nodes) };
|
|
@@ -1337,7 +1361,7 @@ export function extractSpToll(context, fields) {
|
|
|
1337
1361
|
const indexPath = buildAttributeXPath(index);
|
|
1338
1362
|
const fullLocalXPath = `${baseXPath}${indexPath}`;
|
|
1339
1363
|
// @ts-ignore
|
|
1340
|
-
const parentNodes = select(baseXPath, targetDoc);
|
|
1364
|
+
const parentNodes = toNodeArray(select(baseXPath, targetDoc));
|
|
1341
1365
|
// @ts-ignore
|
|
1342
1366
|
const parentAttributes = select(fullLocalXPath, targetDoc).map((n) => n.value);
|
|
1343
1367
|
const childXPath = buildAbsoluteXPath([last(currentLocalPath)].concat(attributePath));
|
|
@@ -1376,7 +1400,7 @@ export function extractSpToll(context, fields) {
|
|
|
1376
1400
|
if (isEntire) {
|
|
1377
1401
|
try {
|
|
1378
1402
|
// @ts-ignore
|
|
1379
|
-
const node = select(baseXPath, targetDoc);
|
|
1403
|
+
const node = toNodeArray(select(baseXPath, targetDoc));
|
|
1380
1404
|
let value = null;
|
|
1381
1405
|
if (node.length === 1) {
|
|
1382
1406
|
value = node[0].toString();
|
|
@@ -1399,7 +1423,7 @@ export function extractSpToll(context, fields) {
|
|
|
1399
1423
|
if (attributes.length > 1 && !listMode) {
|
|
1400
1424
|
try {
|
|
1401
1425
|
// @ts-ignore
|
|
1402
|
-
const baseNodeList = select(baseXPath, targetDoc);
|
|
1426
|
+
const baseNodeList = toNodeArray(select(baseXPath, targetDoc));
|
|
1403
1427
|
if (baseNodeList.length === 0)
|
|
1404
1428
|
return { ...result, [key]: null };
|
|
1405
1429
|
const attributeValues = baseNodeList.map((node) => {
|
|
@@ -1438,7 +1462,7 @@ export function extractSpToll(context, fields) {
|
|
|
1438
1462
|
if (attributes.length === 0 && !listMode) {
|
|
1439
1463
|
try {
|
|
1440
1464
|
// @ts-ignore
|
|
1441
|
-
const node = select(baseXPath, targetDoc);
|
|
1465
|
+
const node = toNodeArray(select(baseXPath, targetDoc));
|
|
1442
1466
|
if (parseCallback) {
|
|
1443
1467
|
// 使用自定义回调函数处理单个节点
|
|
1444
1468
|
return { ...result, [key]: parseCallback(node[0]) };
|
|
@@ -1447,7 +1471,7 @@ export function extractSpToll(context, fields) {
|
|
|
1447
1471
|
if (node.length === 1) {
|
|
1448
1472
|
const fullPath = `string(${baseXPath})`;
|
|
1449
1473
|
// @ts-ignore
|
|
1450
|
-
attributeValue = select(fullPath, targetDoc);
|
|
1474
|
+
attributeValue = toNodeArray(select(fullPath, targetDoc));
|
|
1451
1475
|
}
|
|
1452
1476
|
if (node.length > 1) {
|
|
1453
1477
|
attributeValue = node.filter((n) => n.firstChild)
|
|
@@ -1480,6 +1504,9 @@ export function extractSp(context) {
|
|
|
1480
1504
|
export function extractAuthRequest(context) {
|
|
1481
1505
|
return extract(context, loginRequestFields);
|
|
1482
1506
|
}
|
|
1483
|
-
export function extractResponse(context
|
|
1507
|
+
export function extractResponse(context) {
|
|
1484
1508
|
return extractSpToll(context, loginResponseFieldsFullList);
|
|
1485
1509
|
}
|
|
1510
|
+
export function extractArtifactResolve(context) {
|
|
1511
|
+
return extract(context, artifactResolveFields);
|
|
1512
|
+
}
|