samlesa 2.16.1 → 2.16.6
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.
Potentially problematic release.
This version of samlesa might be problematic. Click here for more details.
- package/build/src/binding-artifact.js +333 -0
- package/build/src/binding-redirect.js +97 -2
- package/build/src/entity-sp.js +138 -21
- package/build/src/extractor.js +13 -0
- package/build/src/flow.js +225 -5
- package/build/src/libsaml.js +233 -2
- package/build/src/metadata-idp.js +22 -0
- package/build/src/metadata-sp.js +19 -15
- package/build/src/metadata.js +50 -31
- package/build/src/schema/saml-schema-ecp-2.0.xsd +1 -1
- package/build/src/schema/saml-schema-metadata-2.0.xsd +3 -3
- package/build/src/schema/saml-schema-protocol-2.0.xsd +1 -1
- package/build/src/schema/soap-envelope.xsd +68 -0
- package/build/src/schema/xml.xsd +88 -0
- package/build/src/schemaValidator.js +28 -6
- package/build/src/soap.js +25 -0
- package/build/src/urn.js +5 -3
- package/package.json +2 -1
- package/types/{binding-post.d.ts → src/binding-artifact.d.ts} +25 -25
- package/types/src/binding-artifact.d.ts.map +1 -0
- package/types/src/binding-redirect.d.ts +14 -1
- package/types/src/binding-redirect.d.ts.map +1 -1
- package/types/src/entity-sp.d.ts +50 -20
- package/types/src/entity-sp.d.ts.map +1 -1
- package/types/src/extractor.d.ts +5 -0
- package/types/src/extractor.d.ts.map +1 -1
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/libsaml.d.ts +16 -0
- package/types/src/libsaml.d.ts.map +1 -1
- package/types/src/metadata-idp.d.ts +6 -0
- package/types/src/metadata-idp.d.ts.map +1 -1
- package/types/src/metadata-sp.d.ts.map +1 -1
- package/types/src/metadata.d.ts +34 -27
- package/types/src/metadata.d.ts.map +1 -1
- package/types/src/schemaValidator.d.ts.map +1 -1
- package/types/src/soap.d.ts +2 -0
- package/types/src/soap.d.ts.map +1 -0
- package/types/src/urn.d.ts +2 -0
- package/types/src/urn.d.ts.map +1 -1
- package/build/.idea/build.iml +0 -12
- package/build/.idea/deployment.xml +0 -14
- package/build/.idea/modules.xml +0 -8
- package/types/api.d.ts +0 -15
- package/types/api.d.ts.map +0 -1
- package/types/binding-post.d.ts.map +0 -1
- package/types/binding-redirect.d.ts +0 -54
- package/types/binding-redirect.d.ts.map +0 -1
- package/types/binding-simplesign.d.ts +0 -41
- package/types/binding-simplesign.d.ts.map +0 -1
- package/types/entity-idp.d.ts +0 -38
- package/types/entity-idp.d.ts.map +0 -1
- package/types/entity-sp.d.ts +0 -38
- package/types/entity-sp.d.ts.map +0 -1
- package/types/entity.d.ts +0 -100
- package/types/entity.d.ts.map +0 -1
- package/types/extractor.d.ts +0 -26
- package/types/extractor.d.ts.map +0 -1
- package/types/flow.d.ts +0 -7
- package/types/flow.d.ts.map +0 -1
- package/types/libsaml.d.ts +0 -208
- package/types/libsaml.d.ts.map +0 -1
- package/types/metadata-idp.d.ts +0 -25
- package/types/metadata-idp.d.ts.map +0 -1
- package/types/metadata-sp.d.ts +0 -37
- package/types/metadata-sp.d.ts.map +0 -1
- package/types/metadata.d.ts +0 -58
- package/types/metadata.d.ts.map +0 -1
- package/types/types.d.ts +0 -128
- package/types/types.d.ts.map +0 -1
- package/types/urn.d.ts +0 -195
- package/types/urn.d.ts.map +0 -1
- package/types/utility.d.ts +0 -133
- package/types/utility.d.ts.map +0 -1
- package/types/validator.d.ts +0 -4
- package/types/validator.d.ts.map +0 -1
package/build/src/extractor.js
CHANGED
|
@@ -68,6 +68,19 @@ export const loginResponseStatusFields = [
|
|
|
68
68
|
}
|
|
69
69
|
];
|
|
70
70
|
// support two-tiers status code
|
|
71
|
+
export const loginArtifactResponseStatusFields = [
|
|
72
|
+
{
|
|
73
|
+
key: 'top',
|
|
74
|
+
localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Status', 'StatusCode'],
|
|
75
|
+
attributes: ['Value'],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: 'second',
|
|
79
|
+
localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Status', 'StatusCode', 'StatusCode'],
|
|
80
|
+
attributes: ['Value'],
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
// support two-tiers status code
|
|
71
84
|
export const logoutResponseStatusFields = [
|
|
72
85
|
{
|
|
73
86
|
key: 'top',
|
package/build/src/flow.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { base64Decode } from './utility.js';
|
|
2
2
|
import { verifyTime } from './validator.js';
|
|
3
3
|
import libsaml from './libsaml.js';
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
4
|
+
import * as uuid from 'uuid';
|
|
5
|
+
import { select } from 'xpath';
|
|
6
|
+
import { DOMParser } from '@xmldom/xmldom';
|
|
7
|
+
import { sendArtifactResolve } from "./soap.js";
|
|
8
|
+
import { extract, loginRequestFields, loginResponseFields, loginResponseStatusFields, loginArtifactResponseStatusFields, logoutRequestFields, logoutResponseFields, logoutResponseStatusFields } from './extractor.js';
|
|
9
|
+
import { BindingNamespace, ParserType, StatusCode, wording } from './urn.js';
|
|
6
10
|
const bindDict = wording.binding;
|
|
7
11
|
const urlParams = wording.urlParams;
|
|
8
12
|
// get the default extractor fields based on the parserType
|
|
@@ -122,6 +126,218 @@ async function redirectFlow(options) {
|
|
|
122
126
|
}
|
|
123
127
|
// proceed the post flow
|
|
124
128
|
async function postFlow(options) {
|
|
129
|
+
const { soap = false, request, from, self, parserType, checkSignature = true } = options;
|
|
130
|
+
const { body } = request;
|
|
131
|
+
const direction = libsaml.getQueryParamByType(parserType);
|
|
132
|
+
let encodedRequest = '';
|
|
133
|
+
let samlContent = '';
|
|
134
|
+
if (soap === false) {
|
|
135
|
+
encodedRequest = body[direction];
|
|
136
|
+
// @ts-ignore
|
|
137
|
+
samlContent = String(base64Decode(encodedRequest));
|
|
138
|
+
}
|
|
139
|
+
/** 增加判断是不是Soap 工件绑定*/
|
|
140
|
+
if (soap) {
|
|
141
|
+
const metadata = {
|
|
142
|
+
idp: from.entityMeta,
|
|
143
|
+
sp: self.entityMeta,
|
|
144
|
+
};
|
|
145
|
+
const spSetting = self.entitySetting;
|
|
146
|
+
let ID = '_' + uuid.v4();
|
|
147
|
+
let url = metadata.idp.getArtifactResolutionService(bindDict.soap);
|
|
148
|
+
let samlSoapRaw = libsaml.replaceTagsByValue(libsaml.defaultArtifactResolveTemplate.context, {
|
|
149
|
+
ID: ID,
|
|
150
|
+
Destination: url,
|
|
151
|
+
Issuer: metadata.sp.getEntityID(),
|
|
152
|
+
IssueInstant: new Date().toISOString(),
|
|
153
|
+
Art: request.Art
|
|
154
|
+
});
|
|
155
|
+
if (!metadata.idp.isWantAuthnRequestsSigned()) {
|
|
156
|
+
samlContent = await sendArtifactResolve(url, samlSoapRaw);
|
|
157
|
+
}
|
|
158
|
+
if (metadata.idp.isWantAuthnRequestsSigned()) {
|
|
159
|
+
const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
|
|
160
|
+
let signatureSoap = libsaml.constructSAMLSignature({
|
|
161
|
+
referenceTagXPath: "//*[local-name(.)='ArtifactResolve']",
|
|
162
|
+
isMessageSigned: false,
|
|
163
|
+
isBase64Output: false,
|
|
164
|
+
transformationAlgorithms: transformationAlgorithms,
|
|
165
|
+
privateKey,
|
|
166
|
+
privateKeyPass,
|
|
167
|
+
signatureAlgorithm,
|
|
168
|
+
rawSamlMessage: samlSoapRaw,
|
|
169
|
+
signingCert: metadata.sp.getX509Certificate('signing'),
|
|
170
|
+
signatureConfig: {
|
|
171
|
+
prefix: 'ds',
|
|
172
|
+
location: {
|
|
173
|
+
reference: "//*[local-name(.)='Issuer']",
|
|
174
|
+
action: 'after'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
samlContent = await sendArtifactResolve(url, signatureSoap);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const verificationOptions = {
|
|
182
|
+
metadata: from.entityMeta,
|
|
183
|
+
signatureAlgorithm: from.entitySetting.requestSignatureAlgorithm,
|
|
184
|
+
};
|
|
185
|
+
/** 断言是否加密应根据响应里面的字段判断*/
|
|
186
|
+
let decryptRequired = from.entitySetting.isAssertionEncrypted;
|
|
187
|
+
let extractorFields = [];
|
|
188
|
+
// validate the xml first
|
|
189
|
+
let res = await libsaml.isValidXml(samlContent).catch((error) => {
|
|
190
|
+
return Promise.reject('ERR_EXCEPTION_VALIDATE_XML');
|
|
191
|
+
});
|
|
192
|
+
if (res !== true) {
|
|
193
|
+
return Promise.reject('ERR_EXCEPTION_VALIDATE_XML');
|
|
194
|
+
}
|
|
195
|
+
if (parserType !== urlParams.samlResponse) {
|
|
196
|
+
extractorFields = getDefaultExtractorFields(parserType, null);
|
|
197
|
+
}
|
|
198
|
+
// check status based on different scenarios
|
|
199
|
+
await checkStatus(samlContent, parserType, soap);
|
|
200
|
+
/**检查签名顺序 */
|
|
201
|
+
/* if (
|
|
202
|
+
checkSignature &&
|
|
203
|
+
from.entitySetting.messageSigningOrder === MessageSignatureOrder.ETS
|
|
204
|
+
) {
|
|
205
|
+
console.log("===============我走的这里=========================")
|
|
206
|
+
const [verified, verifiedAssertionNode,isDecryptRequired] = libsaml.verifySignature(samlContent, verificationOptions);
|
|
207
|
+
console.log(verified);
|
|
208
|
+
console.log("verified")
|
|
209
|
+
decryptRequired = isDecryptRequired
|
|
210
|
+
if (!verified) {
|
|
211
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
|
|
212
|
+
}
|
|
213
|
+
if (!decryptRequired) {
|
|
214
|
+
extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
|
|
215
|
+
}
|
|
216
|
+
}*/
|
|
217
|
+
if (soap === true) {
|
|
218
|
+
const [verified, verifiedAssertionNode, isDecryptRequired] = libsaml.verifySignatureSoap(samlContent, verificationOptions);
|
|
219
|
+
decryptRequired = isDecryptRequired;
|
|
220
|
+
if (!verified) {
|
|
221
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
|
|
222
|
+
}
|
|
223
|
+
if (!decryptRequired) {
|
|
224
|
+
extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
|
|
225
|
+
}
|
|
226
|
+
if (parserType === 'SAMLResponse' && decryptRequired) {
|
|
227
|
+
// 1. 解密断言
|
|
228
|
+
const [decryptedSAML, decryptedAssertion] = await libsaml.decryptAssertionSoap(self, samlContent);
|
|
229
|
+
// 2. 检查解密后的断言是否包含签名
|
|
230
|
+
const assertionDoc = new DOMParser().parseFromString(decryptedAssertion, 'text/xml');
|
|
231
|
+
const assertionSignatureNodes = select("./*[local-name()='Signature']", assertionDoc.documentElement);
|
|
232
|
+
// 3. 如果存在签名则验证
|
|
233
|
+
if (assertionSignatureNodes.length > 0) {
|
|
234
|
+
// 3.1 创建新的验证选项(保持原配置)
|
|
235
|
+
const assertionVerificationOptions = {
|
|
236
|
+
...verificationOptions,
|
|
237
|
+
isAssertion: true // 添加标识表示正在验证断言
|
|
238
|
+
};
|
|
239
|
+
// 3.2 验证断言签名
|
|
240
|
+
const [assertionVerified, result] = libsaml.verifySignatureSoap(decryptedAssertion, assertionVerificationOptions);
|
|
241
|
+
if (!assertionVerified) {
|
|
242
|
+
console.error("解密后的断言签名验证失败");
|
|
243
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ASSERTION_SIGNATURE');
|
|
244
|
+
}
|
|
245
|
+
if (assertionVerified) {
|
|
246
|
+
// @ts-ignore
|
|
247
|
+
samlContent = result;
|
|
248
|
+
extractorFields = getDefaultExtractorFields(parserType, result);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
samlContent = decryptedAssertion;
|
|
253
|
+
extractorFields = getDefaultExtractorFields(parserType, decryptedAssertion);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (soap === false) {
|
|
258
|
+
const [verified, verifiedAssertionNode, isDecryptRequired] = libsaml.verifySignature(samlContent, verificationOptions);
|
|
259
|
+
decryptRequired = isDecryptRequired;
|
|
260
|
+
if (!verified) {
|
|
261
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
|
|
262
|
+
}
|
|
263
|
+
if (!decryptRequired) {
|
|
264
|
+
extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
|
|
265
|
+
}
|
|
266
|
+
if (parserType === 'SAMLResponse' && decryptRequired) {
|
|
267
|
+
const result = await libsaml.decryptAssertion(self, samlContent);
|
|
268
|
+
samlContent = result[0];
|
|
269
|
+
extractorFields = getDefaultExtractorFields(parserType, result[1]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// verify the signatures (the response is signed then encrypted, then decrypt first then verify)
|
|
273
|
+
/* if (
|
|
274
|
+
checkSignature &&
|
|
275
|
+
from.entitySetting.messageSigningOrder === MessageSignatureOrder.STE
|
|
276
|
+
) {
|
|
277
|
+
const [verified, verifiedAssertionNode,isDecryptRequired] = libsaml.verifySignature(samlContent, verificationOptions);
|
|
278
|
+
decryptRequired = isDecryptRequired
|
|
279
|
+
if (verified) {
|
|
280
|
+
extractorFields = getDefaultExtractorFields(parserType, verifiedAssertionNode);
|
|
281
|
+
} else {
|
|
282
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_STE_SIGNATURE');
|
|
283
|
+
}
|
|
284
|
+
}*/
|
|
285
|
+
const parseResult = {
|
|
286
|
+
samlContent: samlContent,
|
|
287
|
+
extract: extract(samlContent, extractorFields),
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* Validation part: validate the context of response after signature is verified and decrypted (optional)
|
|
291
|
+
*/
|
|
292
|
+
const targetEntityMetadata = from.entityMeta;
|
|
293
|
+
const issuer = targetEntityMetadata.getEntityID();
|
|
294
|
+
const extractedProperties = parseResult.extract;
|
|
295
|
+
// unmatched issuer
|
|
296
|
+
if ((parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
|
|
297
|
+
&& extractedProperties
|
|
298
|
+
&& extractedProperties.issuer !== issuer) {
|
|
299
|
+
return Promise.reject('ERR_UNMATCH_ISSUER');
|
|
300
|
+
}
|
|
301
|
+
// invalid session time
|
|
302
|
+
// only run the verifyTime when `SessionNotOnOrAfter` exists
|
|
303
|
+
if (parserType === 'SAMLResponse'
|
|
304
|
+
&& extractedProperties.sessionIndex.sessionNotOnOrAfter
|
|
305
|
+
&& !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
306
|
+
return Promise.reject('ERR_EXPIRED_SESSION');
|
|
307
|
+
}
|
|
308
|
+
// invalid time
|
|
309
|
+
// 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
|
310
|
+
if (parserType === 'SAMLResponse'
|
|
311
|
+
&& extractedProperties.conditions
|
|
312
|
+
&& !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, self.entitySetting.clockDrifts)) {
|
|
313
|
+
return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
|
|
314
|
+
}
|
|
315
|
+
//valid destination
|
|
316
|
+
//There is no validation of the response here. The upper-layer application
|
|
317
|
+
// should verify the result by itself to see if the destination is equal to the SP acs and
|
|
318
|
+
// whether the response.id is used to prevent replay attacks.
|
|
319
|
+
/*
|
|
320
|
+
let destination = extractedProperties?.response?.destination
|
|
321
|
+
let isExit = self.entitySetting?.assertionConsumerService?.filter((item) => {
|
|
322
|
+
return item?.Location === destination
|
|
323
|
+
})
|
|
324
|
+
if (isExit?.length === 0) {
|
|
325
|
+
return Promise.reject('ERR_Destination_URL');
|
|
326
|
+
}
|
|
327
|
+
if (parserType === 'SAMLResponse') {
|
|
328
|
+
let destination = extractedProperties?.response?.destination
|
|
329
|
+
let isExit = self.entitySetting?.assertionConsumerService?.filter((item: { Location: any; }) => {
|
|
330
|
+
return item?.Location === destination
|
|
331
|
+
})
|
|
332
|
+
if (isExit?.length === 0) {
|
|
333
|
+
return Promise.reject('ERR_Destination_URL');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
*/
|
|
337
|
+
return Promise.resolve(parseResult);
|
|
338
|
+
}
|
|
339
|
+
// proceed the post Artifact flow
|
|
340
|
+
async function postArtifactFlow(options) {
|
|
125
341
|
const { request, from, self, parserType, checkSignature = true } = options;
|
|
126
342
|
const { body } = request;
|
|
127
343
|
const direction = libsaml.getQueryParamByType(parserType);
|
|
@@ -330,16 +546,20 @@ async function postSimpleSignFlow(options) {
|
|
|
330
546
|
}
|
|
331
547
|
return Promise.resolve(parseResult);
|
|
332
548
|
}
|
|
333
|
-
function checkStatus(content, parserType) {
|
|
549
|
+
function checkStatus(content, parserType, soap) {
|
|
334
550
|
// only check response parser
|
|
335
551
|
if (parserType !== urlParams.samlResponse && parserType !== urlParams.logoutResponse) {
|
|
336
552
|
return Promise.resolve('SKIPPED');
|
|
337
553
|
}
|
|
338
|
-
|
|
554
|
+
let fields = parserType === urlParams.samlResponse
|
|
339
555
|
? loginResponseStatusFields
|
|
340
556
|
: logoutResponseStatusFields;
|
|
557
|
+
if (soap === true) {
|
|
558
|
+
fields = parserType === urlParams.samlResponse
|
|
559
|
+
? loginArtifactResponseStatusFields
|
|
560
|
+
: logoutResponseStatusFields;
|
|
561
|
+
}
|
|
341
562
|
const { top, second } = extract(content, fields);
|
|
342
|
-
console.log(top, second);
|
|
343
563
|
// only resolve when top-tier status code is success
|
|
344
564
|
if (top === StatusCode.Success) {
|
|
345
565
|
return Promise.resolve('OK');
|
package/build/src/libsaml.js
CHANGED
|
@@ -76,6 +76,16 @@ const libSaml = () => {
|
|
|
76
76
|
const defaultLogoutRequestTemplate = {
|
|
77
77
|
context: '<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}"><saml:Issuer>{Issuer}</saml:Issuer><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID></samlp:LogoutRequest>',
|
|
78
78
|
};
|
|
79
|
+
/**
|
|
80
|
+
* @desc Default art request template
|
|
81
|
+
* @type {LogoutRequestTemplate}
|
|
82
|
+
*/
|
|
83
|
+
const defaultArtifactResolveTemplate = {
|
|
84
|
+
context: `<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><saml2p:ArtifactResolve xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}"><saml2:Issuer>{Issuer}</saml2:Issuer><saml2p:Artifact>{Art}</saml2p:Artifact></saml2p:ArtifactResolve></SOAP-ENV:Body></SOAP-ENV:Envelope>`,
|
|
85
|
+
};
|
|
86
|
+
const defaultArtAuthnRequestTemplate = {
|
|
87
|
+
context: `<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header></SOAP-ENV:Header><samlp:ArtifactResponse xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}" InResponseTo="{InResponseTo}" Version="2.0" IssueInstant="{IssueInstant}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>{AuthnRequest}</samlp:ArtifactResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>`,
|
|
88
|
+
};
|
|
79
89
|
/**
|
|
80
90
|
* @desc Default AttributeStatement template
|
|
81
91
|
* @type {AttributeStatementTemplate}
|
|
@@ -202,6 +212,8 @@ const libSaml = () => {
|
|
|
202
212
|
createXPath,
|
|
203
213
|
getQueryParamByType,
|
|
204
214
|
defaultLoginRequestTemplate,
|
|
215
|
+
defaultArtAuthnRequestTemplate,
|
|
216
|
+
defaultArtifactResolveTemplate,
|
|
205
217
|
defaultLoginResponseTemplate,
|
|
206
218
|
defaultAttributeStatementTemplate,
|
|
207
219
|
defaultAttributeTemplate,
|
|
@@ -445,7 +457,7 @@ const libSaml = () => {
|
|
|
445
457
|
assertionNode = node[0].toString();
|
|
446
458
|
}
|
|
447
459
|
}
|
|
448
|
-
|
|
460
|
+
|
|
449
461
|
if (assertionSignatureNode.length === 1) {
|
|
450
462
|
const verifiedAssertionInfo = extract(assertionSignatureNode[0].toString(), [{
|
|
451
463
|
key: 'refURI',
|
|
@@ -477,9 +489,173 @@ const libSaml = () => {
|
|
|
477
489
|
}]);
|
|
478
490
|
assertionNode = verifiedDoc.assertion.toString();
|
|
479
491
|
}
|
|
480
|
-
|
|
492
|
+
|
|
481
493
|
return [verified, assertionNode];*/
|
|
482
494
|
},
|
|
495
|
+
verifySignatureSoap(xml, opts) {
|
|
496
|
+
const { dom } = getContext();
|
|
497
|
+
const doc = dom.parseFromString(xml);
|
|
498
|
+
const docParser = new DOMParser();
|
|
499
|
+
let selection = [];
|
|
500
|
+
if (opts.isAssertion) {
|
|
501
|
+
// 断言模式下的专用逻辑
|
|
502
|
+
const assertionSignatureXpath = "./*[local-name()='Signature']";
|
|
503
|
+
const signatureNode = select(assertionSignatureXpath, doc.documentElement);
|
|
504
|
+
if (signatureNode.length === 0) {
|
|
505
|
+
throw new Error('ERR_ASSERTION_SIGNATURE_NOT_FOUND');
|
|
506
|
+
}
|
|
507
|
+
selection = selection.concat(signatureNode);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
// 原始的SOAP响应验证逻辑
|
|
511
|
+
const messageSignatureXpath = "/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
512
|
+
"/*[local-name()='ArtifactResponse']/*[local-name()='Signature'] | " +
|
|
513
|
+
"/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
514
|
+
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']/*[local-name()='Signature']";
|
|
515
|
+
const assertionSignatureXpath = "/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
516
|
+
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
|
|
517
|
+
"/*[local-name()='Assertion']/*[local-name()='Signature'] | " +
|
|
518
|
+
"/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
519
|
+
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
|
|
520
|
+
"/*[local-name()='EncryptedAssertion']";
|
|
521
|
+
const wrappingElementsXPath = "/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
522
|
+
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
|
|
523
|
+
"/*[local-name()='Assertion']/*[local-name()='Subject']" +
|
|
524
|
+
"/*[local-name()='SubjectConfirmation']" +
|
|
525
|
+
"/*[local-name()='SubjectConfirmationData']" +
|
|
526
|
+
"//*[local-name()='Assertion' or local-name()='Signature']";
|
|
527
|
+
const messageSignatureNode = select(messageSignatureXpath, doc);
|
|
528
|
+
const assertionSignatureNode = select(assertionSignatureXpath, doc);
|
|
529
|
+
const wrappingElementNode = select(wrappingElementsXPath, doc);
|
|
530
|
+
// 检测包装攻击
|
|
531
|
+
if (wrappingElementNode.length !== 0) {
|
|
532
|
+
throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
|
|
533
|
+
}
|
|
534
|
+
// 保证响应中至少有一个签名
|
|
535
|
+
if (messageSignatureNode.length === 0 && assertionSignatureNode.length === 0) {
|
|
536
|
+
throw new Error('ERR_ZERO_SIGNATURE');
|
|
537
|
+
}
|
|
538
|
+
selection = selection.concat(messageSignatureNode, assertionSignatureNode);
|
|
539
|
+
}
|
|
540
|
+
for (const signatureNode of selection) {
|
|
541
|
+
const sig = new SignedXml();
|
|
542
|
+
let verified = false;
|
|
543
|
+
sig.signatureAlgorithm = opts.signatureAlgorithm;
|
|
544
|
+
if (!opts.keyFile && !opts.metadata) {
|
|
545
|
+
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
546
|
+
}
|
|
547
|
+
if (opts.keyFile) {
|
|
548
|
+
sig.publicCert = fs.readFileSync(opts.keyFile, 'utf-8');
|
|
549
|
+
}
|
|
550
|
+
if (opts.metadata) {
|
|
551
|
+
const certificateNodes = select(".//*[local-name(.)='X509Certificate']", signatureNode);
|
|
552
|
+
// 获取元数据中的证书
|
|
553
|
+
let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
|
|
554
|
+
// 规范化元数据证书
|
|
555
|
+
if (Array.isArray(metadataCert)) {
|
|
556
|
+
metadataCert = flattenDeep(metadataCert);
|
|
557
|
+
}
|
|
558
|
+
else if (typeof metadataCert === 'string') {
|
|
559
|
+
metadataCert = [metadataCert];
|
|
560
|
+
}
|
|
561
|
+
metadataCert = metadataCert.map(utility.normalizeCerString);
|
|
562
|
+
// 检查证书可用性
|
|
563
|
+
if (certificateNodes.length === 0 && metadataCert.length === 0) {
|
|
564
|
+
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
565
|
+
}
|
|
566
|
+
// 响应中有证书节点
|
|
567
|
+
if (certificateNodes.length !== 0) {
|
|
568
|
+
// 安全获取证书数据
|
|
569
|
+
let x509CertificateData = '';
|
|
570
|
+
if (certificateNodes[0].firstChild) {
|
|
571
|
+
x509CertificateData = certificateNodes[0].firstChild.data;
|
|
572
|
+
}
|
|
573
|
+
else if (certificateNodes[0].textContent) {
|
|
574
|
+
x509CertificateData = certificateNodes[0].textContent;
|
|
575
|
+
}
|
|
576
|
+
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
577
|
+
// 验证证书匹配
|
|
578
|
+
if (metadataCert.length >= 1 &&
|
|
579
|
+
!metadataCert.find(cert => cert.trim() === x509Certificate.trim())) {
|
|
580
|
+
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
581
|
+
}
|
|
582
|
+
sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
// 使用元数据中的第一个证书
|
|
586
|
+
sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// 加载签名
|
|
590
|
+
sig.loadSignature(signatureNode);
|
|
591
|
+
// 使用原始 XML 进行验证
|
|
592
|
+
verified = sig.checkSignature(xml);
|
|
593
|
+
console.log("签名验证结果:", verified);
|
|
594
|
+
if (!verified) {
|
|
595
|
+
console.error("签名验证失败");
|
|
596
|
+
throw new Error('ERR_FAILED_TO_VERIFY_SIGNATURE');
|
|
597
|
+
}
|
|
598
|
+
// 检查签名引用
|
|
599
|
+
if (!(sig.getSignedReferences().length >= 1)) {
|
|
600
|
+
throw new Error('NO_SIGNATURE_REFERENCES');
|
|
601
|
+
}
|
|
602
|
+
const signedVerifiedXML = sig.getSignedReferences()[0];
|
|
603
|
+
const verifiedDoc = docParser.parseFromString(signedVerifiedXML, 'text/xml');
|
|
604
|
+
const rootNode = verifiedDoc.documentElement;
|
|
605
|
+
console.log("签名引用根节点:", rootNode.localName);
|
|
606
|
+
// 断言模式专用返回逻辑
|
|
607
|
+
if (opts.isAssertion) {
|
|
608
|
+
if (rootNode.localName === 'Assertion') {
|
|
609
|
+
return [true, rootNode.toString(), false];
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
throw new Error('ERR_INVALID_ASSERTION_SIGNATURE');
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// 处理已验证的签名
|
|
616
|
+
if (rootNode.localName === 'ArtifactResponse') {
|
|
617
|
+
// 在 ArtifactResponse 中查找 Response
|
|
618
|
+
const responseNodes = select("./*[local-name()='Response']", rootNode);
|
|
619
|
+
if (responseNodes.length === 0) {
|
|
620
|
+
console.warn("ArtifactResponse 中没有找到 Response 元素");
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const responseNode = responseNodes[0];
|
|
624
|
+
// 在 Response 中查找断言
|
|
625
|
+
const encryptedAssertions = select("./*[local-name()='EncryptedAssertion']", responseNode);
|
|
626
|
+
const assertions = select("./*[local-name()='Assertion']", responseNode);
|
|
627
|
+
if (encryptedAssertions.length === 1) {
|
|
628
|
+
return [true, encryptedAssertions[0].toString(), true];
|
|
629
|
+
}
|
|
630
|
+
if (assertions.length === 1) {
|
|
631
|
+
return [true, assertions[0].toString(), false];
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// 直接处理 Response
|
|
635
|
+
else if (rootNode.localName === 'Response') {
|
|
636
|
+
const encryptedAssertions = select("./*[local-name()='EncryptedAssertion']", rootNode);
|
|
637
|
+
const assertions = select("./*[local-name()='Assertion']", rootNode);
|
|
638
|
+
if (encryptedAssertions.length === 1) {
|
|
639
|
+
return [true, encryptedAssertions[0].toString(), true];
|
|
640
|
+
}
|
|
641
|
+
if (assertions.length === 1) {
|
|
642
|
+
return [true, assertions[0].toString(), false];
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// 直接处理 Assertion
|
|
646
|
+
else if (rootNode.localName === 'Assertion') {
|
|
647
|
+
return [true, rootNode.toString(), false];
|
|
648
|
+
}
|
|
649
|
+
// 直接处理 EncryptedAssertion
|
|
650
|
+
else if (rootNode.localName === 'EncryptedAssertion') {
|
|
651
|
+
return [true, rootNode.toString(), true];
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
console.warn("未知的根节点类型:", rootNode.localName);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
throw new Error('ERR_ZERO_SIGNATURE');
|
|
658
|
+
},
|
|
483
659
|
/**
|
|
484
660
|
* @desc Helper function to create the key section in metadata (abstraction for signing and encrypt use)
|
|
485
661
|
* @param {string} use type of certificate (e.g. signing, encrypt)
|
|
@@ -686,6 +862,61 @@ const libSaml = () => {
|
|
|
686
862
|
});
|
|
687
863
|
});
|
|
688
864
|
},
|
|
865
|
+
/**
|
|
866
|
+
* 解密 SOAP 响应中的加密断言
|
|
867
|
+
* @param self 当前实体(SP 或 IdP)
|
|
868
|
+
* @param entireXML 完整的 SOAP XML 响应
|
|
869
|
+
* @returns [解密后的完整 SOAP XML, 解密后的断言 XML]
|
|
870
|
+
*/
|
|
871
|
+
async decryptAssertionSoap(self, entireXML) {
|
|
872
|
+
const { dom } = getContext();
|
|
873
|
+
try {
|
|
874
|
+
// 1. 解析 XML
|
|
875
|
+
const doc = dom.parseFromString(entireXML);
|
|
876
|
+
// 2. 定位加密断言
|
|
877
|
+
const encryptedAssertions = select("/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
878
|
+
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
|
|
879
|
+
"/*[local-name()='EncryptedAssertion']", doc);
|
|
880
|
+
if (!encryptedAssertions || encryptedAssertions.length === 0) {
|
|
881
|
+
throw new Error('ERR_ENCRYPTED_ASSERTION_NOT_FOUND');
|
|
882
|
+
}
|
|
883
|
+
if (encryptedAssertions.length > 1) {
|
|
884
|
+
console.warn('发现多个加密断言,仅处理第一个');
|
|
885
|
+
}
|
|
886
|
+
const encAssertionNode = encryptedAssertions[0];
|
|
887
|
+
// 3. 准备解密密钥
|
|
888
|
+
const privateKey = utility.readPrivateKey(self.entitySetting.encPrivateKey, self.entitySetting.encPrivateKeyPass);
|
|
889
|
+
// 4. 解密断言
|
|
890
|
+
const decryptedAssertion = await new Promise((resolve, reject) => {
|
|
891
|
+
xmlenc.decrypt(encAssertionNode.toString(), { key: privateKey }, (err, result) => {
|
|
892
|
+
if (err) {
|
|
893
|
+
console.error('解密错误:', err);
|
|
894
|
+
return reject(new Error('ERR_ASSERTION_DECRYPTION_FAILED'));
|
|
895
|
+
}
|
|
896
|
+
if (!result) {
|
|
897
|
+
return reject(new Error('ERR_EMPTY_DECRYPTED_ASSERTION'));
|
|
898
|
+
}
|
|
899
|
+
resolve(result);
|
|
900
|
+
});
|
|
901
|
+
});
|
|
902
|
+
// 5. 创建解密断言的 DOM
|
|
903
|
+
const decryptedDoc = dom.parseFromString(decryptedAssertion);
|
|
904
|
+
const decryptedAssertionNode = decryptedDoc.documentElement;
|
|
905
|
+
// 6. 替换加密断言为解密后的断言
|
|
906
|
+
const parentNode = encAssertionNode.parentNode;
|
|
907
|
+
if (!parentNode) {
|
|
908
|
+
throw new Error('ERR_NO_PARENT_NODE_FOR_ENCRYPTED_ASSERTION');
|
|
909
|
+
}
|
|
910
|
+
parentNode.replaceChild(decryptedAssertionNode, encAssertionNode);
|
|
911
|
+
// 7. 序列化更新后的文档
|
|
912
|
+
const updatedSoapXml = doc.toString();
|
|
913
|
+
return [updatedSoapXml, decryptedAssertion];
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
console.error('SOAP断言解密失败:', error);
|
|
917
|
+
throw new Error('ERR_SOAP_ASSERTION_DECRYPTION');
|
|
918
|
+
}
|
|
919
|
+
},
|
|
689
920
|
/**
|
|
690
921
|
* @desc Check if the xml string is valid and bounded
|
|
691
922
|
*/
|
|
@@ -102,6 +102,13 @@ export class IdpMetadata extends Metadata {
|
|
|
102
102
|
attributePath: [],
|
|
103
103
|
attributes: ['Location']
|
|
104
104
|
},
|
|
105
|
+
{
|
|
106
|
+
key: 'artifactResolutionService',
|
|
107
|
+
localPath: ['EntityDescriptor', 'IDPSSODescriptor', 'ArtifactResolutionService'],
|
|
108
|
+
index: ['Binding'],
|
|
109
|
+
attributePath: [],
|
|
110
|
+
attributes: ['Location']
|
|
111
|
+
},
|
|
105
112
|
]);
|
|
106
113
|
}
|
|
107
114
|
/**
|
|
@@ -130,4 +137,19 @@ export class IdpMetadata extends Metadata {
|
|
|
130
137
|
}
|
|
131
138
|
return this.meta.singleSignOnService;
|
|
132
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* @desc Get the entity endpoint for single ArtifactResolutionService
|
|
142
|
+
* @param {string} binding protocol binding (e.g. redirect, post)
|
|
143
|
+
* @return {string/object} location
|
|
144
|
+
*/
|
|
145
|
+
getArtifactResolutionService(binding) {
|
|
146
|
+
if (isString(binding)) {
|
|
147
|
+
const bindName = namespace.binding[binding];
|
|
148
|
+
const service = this.meta.artifactResolutionService[bindName];
|
|
149
|
+
if (service) {
|
|
150
|
+
return service;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return this.meta.artifactResolutionService;
|
|
154
|
+
}
|
|
133
155
|
}
|
package/build/src/metadata-sp.js
CHANGED
|
@@ -70,6 +70,20 @@ export class SpMetadata extends Metadata {
|
|
|
70
70
|
descriptors.SingleLogoutService.push([{ _attr: attr }]);
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
+
if (isNonEmptyArray(artifactResolutionService)) {
|
|
74
|
+
let indexCount = 0;
|
|
75
|
+
artifactResolutionService.forEach(a => {
|
|
76
|
+
const attr = {
|
|
77
|
+
index: String(indexCount++),
|
|
78
|
+
Binding: a.Binding,
|
|
79
|
+
Location: a.Location,
|
|
80
|
+
};
|
|
81
|
+
if (a.isDefault) {
|
|
82
|
+
attr.isDefault = true;
|
|
83
|
+
}
|
|
84
|
+
descriptors.ArtifactResolutionService.push([{ _attr: attr }]);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
73
87
|
if (isNonEmptyArray(assertionConsumerService)) {
|
|
74
88
|
let indexCount = 0;
|
|
75
89
|
assertionConsumerService.forEach(a => {
|
|
@@ -150,21 +164,6 @@ export class SpMetadata extends Metadata {
|
|
|
150
164
|
descriptors.AttributeConsumingService.push(attrConsumingService);
|
|
151
165
|
});
|
|
152
166
|
}
|
|
153
|
-
if (isNonEmptyArray(artifactResolutionService)) {
|
|
154
|
-
artifactResolutionService.forEach(a => {
|
|
155
|
-
const attr = {
|
|
156
|
-
Binding: a.Binding,
|
|
157
|
-
Location: a.Location,
|
|
158
|
-
};
|
|
159
|
-
if (a.isDefault) {
|
|
160
|
-
attr.isDefault = true;
|
|
161
|
-
}
|
|
162
|
-
descriptors.ArtifactResolutionService.push([{ _attr: attr }]);
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
console.warn('Construct identity provider - missing endpoint of ArtifactResolutionService');
|
|
167
|
-
}
|
|
168
167
|
// handle element order
|
|
169
168
|
const existedElements = elementsOrder.filter(name => isNonEmptyArray(descriptors[name]));
|
|
170
169
|
existedElements.forEach(name => {
|
|
@@ -193,6 +192,11 @@ export class SpMetadata extends Metadata {
|
|
|
193
192
|
key: 'assertionConsumerService',
|
|
194
193
|
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AssertionConsumerService'],
|
|
195
194
|
attributes: ['Binding', 'Location', 'isDefault', 'index'],
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
key: 'artifactResolutionService',
|
|
198
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'ArtifactResolutionService'],
|
|
199
|
+
attributes: ['Binding', 'Location', 'isDefault', 'index'],
|
|
196
200
|
}
|
|
197
201
|
]);
|
|
198
202
|
}
|