samlesa 2.16.0 → 2.16.5
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/entity-sp.js +23 -0
- package/build/src/flow.js +235 -1
- package/build/src/libsaml.js +254 -1
- package/build/src/metadata-idp.js +22 -0
- package/build/src/metadata-sp.js +17 -15
- package/build/src/metadata.js +52 -31
- package/build/src/schema/env.xsd +100 -0
- package/build/src/schemaValidator.js +29 -1
- package/build/src/soap.js +25 -0
- package/build/src/urn.js +5 -3
- package/package.json +3 -2
- package/types/src/binding-artifact.d.ts +48 -0
- package/types/src/binding-artifact.d.ts.map +1 -0
- package/types/src/entity-sp.d.ts +7 -0
- package/types/src/entity-sp.d.ts.map +1 -1
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/libsaml.d.ts +13 -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/src/libsaml.js
CHANGED
|
@@ -70,12 +70,19 @@ const libSaml = () => {
|
|
|
70
70
|
context: '<samlp:AuthnRequest 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}" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="{AssertionConsumerServiceURL}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:NameIDPolicy Format="{NameIDFormat}" AllowCreate="{AllowCreate}"/></samlp:AuthnRequest>',
|
|
71
71
|
};
|
|
72
72
|
/**
|
|
73
|
-
* @desc Default
|
|
73
|
+
* @desc Default art request template
|
|
74
74
|
* @type {LogoutRequestTemplate}
|
|
75
75
|
*/
|
|
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 logout 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
|
+
};
|
|
79
86
|
/**
|
|
80
87
|
* @desc Default AttributeStatement template
|
|
81
88
|
* @type {AttributeStatementTemplate}
|
|
@@ -152,6 +159,12 @@ const libSaml = () => {
|
|
|
152
159
|
}
|
|
153
160
|
return nrsaAliasMappingForNode[signatureAlgorithms.RSA_SHA256];
|
|
154
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* @private
|
|
164
|
+
* @desc Get the signing scheme alias by signature algorithms, used by the node-rsa module
|
|
165
|
+
* @param {string} sigAlg signature algorithm
|
|
166
|
+
* @return {string/null} signing algorithm short-hand for the module node-rsa
|
|
167
|
+
*/
|
|
155
168
|
/**
|
|
156
169
|
* @private
|
|
157
170
|
* @desc Get the digest algorithms by signature algorithms
|
|
@@ -196,6 +209,7 @@ const libSaml = () => {
|
|
|
196
209
|
createXPath,
|
|
197
210
|
getQueryParamByType,
|
|
198
211
|
defaultLoginRequestTemplate,
|
|
212
|
+
defaultArtifactResolveTemplate,
|
|
199
213
|
defaultLoginResponseTemplate,
|
|
200
214
|
defaultAttributeStatementTemplate,
|
|
201
215
|
defaultAttributeTemplate,
|
|
@@ -474,6 +488,170 @@ const libSaml = () => {
|
|
|
474
488
|
|
|
475
489
|
return [verified, assertionNode];*/
|
|
476
490
|
},
|
|
491
|
+
verifySignatureSoap(xml, opts) {
|
|
492
|
+
const { dom } = getContext();
|
|
493
|
+
const doc = dom.parseFromString(xml);
|
|
494
|
+
const docParser = new DOMParser();
|
|
495
|
+
let selection = [];
|
|
496
|
+
if (opts.isAssertion) {
|
|
497
|
+
// 断言模式下的专用逻辑
|
|
498
|
+
const assertionSignatureXpath = "./*[local-name()='Signature']";
|
|
499
|
+
const signatureNode = select(assertionSignatureXpath, doc.documentElement);
|
|
500
|
+
if (signatureNode.length === 0) {
|
|
501
|
+
throw new Error('ERR_ASSERTION_SIGNATURE_NOT_FOUND');
|
|
502
|
+
}
|
|
503
|
+
selection = selection.concat(signatureNode);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
// 原始的SOAP响应验证逻辑
|
|
507
|
+
const messageSignatureXpath = "/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
508
|
+
"/*[local-name()='ArtifactResponse']/*[local-name()='Signature'] | " +
|
|
509
|
+
"/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
510
|
+
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']/*[local-name()='Signature']";
|
|
511
|
+
const assertionSignatureXpath = "/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
512
|
+
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
|
|
513
|
+
"/*[local-name()='Assertion']/*[local-name()='Signature'] | " +
|
|
514
|
+
"/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
515
|
+
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
|
|
516
|
+
"/*[local-name()='EncryptedAssertion']";
|
|
517
|
+
const wrappingElementsXPath = "/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
518
|
+
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
|
|
519
|
+
"/*[local-name()='Assertion']/*[local-name()='Subject']" +
|
|
520
|
+
"/*[local-name()='SubjectConfirmation']" +
|
|
521
|
+
"/*[local-name()='SubjectConfirmationData']" +
|
|
522
|
+
"//*[local-name()='Assertion' or local-name()='Signature']";
|
|
523
|
+
const messageSignatureNode = select(messageSignatureXpath, doc);
|
|
524
|
+
const assertionSignatureNode = select(assertionSignatureXpath, doc);
|
|
525
|
+
const wrappingElementNode = select(wrappingElementsXPath, doc);
|
|
526
|
+
// 检测包装攻击
|
|
527
|
+
if (wrappingElementNode.length !== 0) {
|
|
528
|
+
throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
|
|
529
|
+
}
|
|
530
|
+
// 保证响应中至少有一个签名
|
|
531
|
+
if (messageSignatureNode.length === 0 && assertionSignatureNode.length === 0) {
|
|
532
|
+
throw new Error('ERR_ZERO_SIGNATURE');
|
|
533
|
+
}
|
|
534
|
+
selection = selection.concat(messageSignatureNode, assertionSignatureNode);
|
|
535
|
+
}
|
|
536
|
+
for (const signatureNode of selection) {
|
|
537
|
+
const sig = new SignedXml();
|
|
538
|
+
let verified = false;
|
|
539
|
+
sig.signatureAlgorithm = opts.signatureAlgorithm;
|
|
540
|
+
if (!opts.keyFile && !opts.metadata) {
|
|
541
|
+
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
542
|
+
}
|
|
543
|
+
if (opts.keyFile) {
|
|
544
|
+
sig.publicCert = fs.readFileSync(opts.keyFile, 'utf-8');
|
|
545
|
+
}
|
|
546
|
+
if (opts.metadata) {
|
|
547
|
+
const certificateNodes = select(".//*[local-name(.)='X509Certificate']", signatureNode);
|
|
548
|
+
// 获取元数据中的证书
|
|
549
|
+
let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
|
|
550
|
+
// 规范化元数据证书
|
|
551
|
+
if (Array.isArray(metadataCert)) {
|
|
552
|
+
metadataCert = flattenDeep(metadataCert);
|
|
553
|
+
}
|
|
554
|
+
else if (typeof metadataCert === 'string') {
|
|
555
|
+
metadataCert = [metadataCert];
|
|
556
|
+
}
|
|
557
|
+
metadataCert = metadataCert.map(utility.normalizeCerString);
|
|
558
|
+
// 检查证书可用性
|
|
559
|
+
if (certificateNodes.length === 0 && metadataCert.length === 0) {
|
|
560
|
+
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
561
|
+
}
|
|
562
|
+
// 响应中有证书节点
|
|
563
|
+
if (certificateNodes.length !== 0) {
|
|
564
|
+
// 安全获取证书数据
|
|
565
|
+
let x509CertificateData = '';
|
|
566
|
+
if (certificateNodes[0].firstChild) {
|
|
567
|
+
x509CertificateData = certificateNodes[0].firstChild.data;
|
|
568
|
+
}
|
|
569
|
+
else if (certificateNodes[0].textContent) {
|
|
570
|
+
x509CertificateData = certificateNodes[0].textContent;
|
|
571
|
+
}
|
|
572
|
+
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
573
|
+
// 验证证书匹配
|
|
574
|
+
if (metadataCert.length >= 1 &&
|
|
575
|
+
!metadataCert.find(cert => cert.trim() === x509Certificate.trim())) {
|
|
576
|
+
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
577
|
+
}
|
|
578
|
+
sig.publicCert = this.getKeyInfo(x509Certificate).getKey();
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
// 使用元数据中的第一个证书
|
|
582
|
+
sig.publicCert = this.getKeyInfo(metadataCert[0]).getKey();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// 加载签名
|
|
586
|
+
sig.loadSignature(signatureNode);
|
|
587
|
+
// 使用原始 XML 进行验证
|
|
588
|
+
verified = sig.checkSignature(xml);
|
|
589
|
+
console.log("签名验证结果:", verified);
|
|
590
|
+
if (!verified) {
|
|
591
|
+
console.error("签名验证失败");
|
|
592
|
+
throw new Error('ERR_FAILED_TO_VERIFY_SIGNATURE');
|
|
593
|
+
}
|
|
594
|
+
// 检查签名引用
|
|
595
|
+
if (!(sig.getSignedReferences().length >= 1)) {
|
|
596
|
+
throw new Error('NO_SIGNATURE_REFERENCES');
|
|
597
|
+
}
|
|
598
|
+
const signedVerifiedXML = sig.getSignedReferences()[0];
|
|
599
|
+
const verifiedDoc = docParser.parseFromString(signedVerifiedXML, 'text/xml');
|
|
600
|
+
const rootNode = verifiedDoc.documentElement;
|
|
601
|
+
console.log("签名引用根节点:", rootNode.localName);
|
|
602
|
+
// 断言模式专用返回逻辑
|
|
603
|
+
if (opts.isAssertion) {
|
|
604
|
+
if (rootNode.localName === 'Assertion') {
|
|
605
|
+
return [true, rootNode.toString(), false];
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
throw new Error('ERR_INVALID_ASSERTION_SIGNATURE');
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// 处理已验证的签名
|
|
612
|
+
if (rootNode.localName === 'ArtifactResponse') {
|
|
613
|
+
// 在 ArtifactResponse 中查找 Response
|
|
614
|
+
const responseNodes = select("./*[local-name()='Response']", rootNode);
|
|
615
|
+
if (responseNodes.length === 0) {
|
|
616
|
+
console.warn("ArtifactResponse 中没有找到 Response 元素");
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
const responseNode = responseNodes[0];
|
|
620
|
+
// 在 Response 中查找断言
|
|
621
|
+
const encryptedAssertions = select("./*[local-name()='EncryptedAssertion']", responseNode);
|
|
622
|
+
const assertions = select("./*[local-name()='Assertion']", responseNode);
|
|
623
|
+
if (encryptedAssertions.length === 1) {
|
|
624
|
+
return [true, encryptedAssertions[0].toString(), true];
|
|
625
|
+
}
|
|
626
|
+
if (assertions.length === 1) {
|
|
627
|
+
return [true, assertions[0].toString(), false];
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// 直接处理 Response
|
|
631
|
+
else if (rootNode.localName === 'Response') {
|
|
632
|
+
const encryptedAssertions = select("./*[local-name()='EncryptedAssertion']", rootNode);
|
|
633
|
+
const assertions = select("./*[local-name()='Assertion']", rootNode);
|
|
634
|
+
if (encryptedAssertions.length === 1) {
|
|
635
|
+
return [true, encryptedAssertions[0].toString(), true];
|
|
636
|
+
}
|
|
637
|
+
if (assertions.length === 1) {
|
|
638
|
+
return [true, assertions[0].toString(), false];
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// 直接处理 Assertion
|
|
642
|
+
else if (rootNode.localName === 'Assertion') {
|
|
643
|
+
return [true, rootNode.toString(), false];
|
|
644
|
+
}
|
|
645
|
+
// 直接处理 EncryptedAssertion
|
|
646
|
+
else if (rootNode.localName === 'EncryptedAssertion') {
|
|
647
|
+
return [true, rootNode.toString(), true];
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
console.warn("未知的根节点类型:", rootNode.localName);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
throw new Error('ERR_ZERO_SIGNATURE');
|
|
654
|
+
},
|
|
477
655
|
/**
|
|
478
656
|
* @desc Helper function to create the key section in metadata (abstraction for signing and encrypt use)
|
|
479
657
|
* @param {string} use type of certificate (e.g. signing, encrypt)
|
|
@@ -538,6 +716,26 @@ const libSaml = () => {
|
|
|
538
716
|
throw new Error(`SAML 签名失败: ${error.message}`);
|
|
539
717
|
}
|
|
540
718
|
},
|
|
719
|
+
/* constructMessageSignature(
|
|
720
|
+
octetString: string,
|
|
721
|
+
key: string,
|
|
722
|
+
passphrase?: string,
|
|
723
|
+
isBase64?: boolean,
|
|
724
|
+
signingAlgorithm?: string
|
|
725
|
+
) {
|
|
726
|
+
// Default returning base64 encoded signature
|
|
727
|
+
// Embed with node-rsa module
|
|
728
|
+
const decryptedKey = new nrsa(
|
|
729
|
+
utility.readPrivateKey(key, passphrase),
|
|
730
|
+
undefined,
|
|
731
|
+
{
|
|
732
|
+
signingScheme: getSigningScheme(signingAlgorithm),
|
|
733
|
+
}
|
|
734
|
+
);
|
|
735
|
+
const signature = decryptedKey.sign(octetString);
|
|
736
|
+
// Use private key to sign data
|
|
737
|
+
return isBase64 !== false ? signature.toString('base64') : signature;
|
|
738
|
+
},*/
|
|
541
739
|
verifyMessageSignature(metadata, octetString, signature, verifyAlgorithm) {
|
|
542
740
|
const signCert = metadata.getX509Certificate(certUse.signing);
|
|
543
741
|
const signingScheme = getSigningSchemeForNode(verifyAlgorithm);
|
|
@@ -660,6 +858,61 @@ const libSaml = () => {
|
|
|
660
858
|
});
|
|
661
859
|
});
|
|
662
860
|
},
|
|
861
|
+
/**
|
|
862
|
+
* 解密 SOAP 响应中的加密断言
|
|
863
|
+
* @param self 当前实体(SP 或 IdP)
|
|
864
|
+
* @param entireXML 完整的 SOAP XML 响应
|
|
865
|
+
* @returns [解密后的完整 SOAP XML, 解密后的断言 XML]
|
|
866
|
+
*/
|
|
867
|
+
async decryptAssertionSoap(self, entireXML) {
|
|
868
|
+
const { dom } = getContext();
|
|
869
|
+
try {
|
|
870
|
+
// 1. 解析 XML
|
|
871
|
+
const doc = dom.parseFromString(entireXML);
|
|
872
|
+
// 2. 定位加密断言
|
|
873
|
+
const encryptedAssertions = select("/*[local-name()='Envelope']/*[local-name()='Body']" +
|
|
874
|
+
"/*[local-name()='ArtifactResponse']/*[local-name()='Response']" +
|
|
875
|
+
"/*[local-name()='EncryptedAssertion']", doc);
|
|
876
|
+
if (!encryptedAssertions || encryptedAssertions.length === 0) {
|
|
877
|
+
throw new Error('ERR_ENCRYPTED_ASSERTION_NOT_FOUND');
|
|
878
|
+
}
|
|
879
|
+
if (encryptedAssertions.length > 1) {
|
|
880
|
+
console.warn('发现多个加密断言,仅处理第一个');
|
|
881
|
+
}
|
|
882
|
+
const encAssertionNode = encryptedAssertions[0];
|
|
883
|
+
// 3. 准备解密密钥
|
|
884
|
+
const privateKey = utility.readPrivateKey(self.entitySetting.encPrivateKey, self.entitySetting.encPrivateKeyPass);
|
|
885
|
+
// 4. 解密断言
|
|
886
|
+
const decryptedAssertion = await new Promise((resolve, reject) => {
|
|
887
|
+
xmlenc.decrypt(encAssertionNode.toString(), { key: privateKey }, (err, result) => {
|
|
888
|
+
if (err) {
|
|
889
|
+
console.error('解密错误:', err);
|
|
890
|
+
return reject(new Error('ERR_ASSERTION_DECRYPTION_FAILED'));
|
|
891
|
+
}
|
|
892
|
+
if (!result) {
|
|
893
|
+
return reject(new Error('ERR_EMPTY_DECRYPTED_ASSERTION'));
|
|
894
|
+
}
|
|
895
|
+
resolve(result);
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
// 5. 创建解密断言的 DOM
|
|
899
|
+
const decryptedDoc = dom.parseFromString(decryptedAssertion);
|
|
900
|
+
const decryptedAssertionNode = decryptedDoc.documentElement;
|
|
901
|
+
// 6. 替换加密断言为解密后的断言
|
|
902
|
+
const parentNode = encAssertionNode.parentNode;
|
|
903
|
+
if (!parentNode) {
|
|
904
|
+
throw new Error('ERR_NO_PARENT_NODE_FOR_ENCRYPTED_ASSERTION');
|
|
905
|
+
}
|
|
906
|
+
parentNode.replaceChild(decryptedAssertionNode, encAssertionNode);
|
|
907
|
+
// 7. 序列化更新后的文档
|
|
908
|
+
const updatedSoapXml = doc.toString();
|
|
909
|
+
return [updatedSoapXml, decryptedAssertion];
|
|
910
|
+
}
|
|
911
|
+
catch (error) {
|
|
912
|
+
console.error('SOAP断言解密失败:', error);
|
|
913
|
+
throw new Error('ERR_SOAP_ASSERTION_DECRYPTION');
|
|
914
|
+
}
|
|
915
|
+
},
|
|
663
916
|
/**
|
|
664
917
|
* @desc Check if the xml string is valid and bounded
|
|
665
918
|
*/
|
|
@@ -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,18 @@ export class SpMetadata extends Metadata {
|
|
|
70
70
|
descriptors.SingleLogoutService.push([{ _attr: attr }]);
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
+
if (isNonEmptyArray(artifactResolutionService)) {
|
|
74
|
+
artifactResolutionService.forEach(a => {
|
|
75
|
+
const attr = {
|
|
76
|
+
Binding: a.Binding,
|
|
77
|
+
Location: a.Location,
|
|
78
|
+
};
|
|
79
|
+
if (a.isDefault) {
|
|
80
|
+
attr.isDefault = true;
|
|
81
|
+
}
|
|
82
|
+
descriptors.ArtifactResolutionService.push([{ _attr: attr }]);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
73
85
|
if (isNonEmptyArray(assertionConsumerService)) {
|
|
74
86
|
let indexCount = 0;
|
|
75
87
|
assertionConsumerService.forEach(a => {
|
|
@@ -150,21 +162,6 @@ export class SpMetadata extends Metadata {
|
|
|
150
162
|
descriptors.AttributeConsumingService.push(attrConsumingService);
|
|
151
163
|
});
|
|
152
164
|
}
|
|
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
165
|
// handle element order
|
|
169
166
|
const existedElements = elementsOrder.filter(name => isNonEmptyArray(descriptors[name]));
|
|
170
167
|
existedElements.forEach(name => {
|
|
@@ -193,6 +190,11 @@ export class SpMetadata extends Metadata {
|
|
|
193
190
|
key: 'assertionConsumerService',
|
|
194
191
|
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'AssertionConsumerService'],
|
|
195
192
|
attributes: ['Binding', 'Location', 'isDefault', 'index'],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
key: 'artifactResolutionService',
|
|
196
|
+
localPath: ['EntityDescriptor', 'SPSSODescriptor', 'ArtifactResolutionService'],
|
|
197
|
+
attributes: ['Binding', 'Location', 'isDefault', 'index'],
|
|
196
198
|
}
|
|
197
199
|
]);
|
|
198
200
|
}
|
package/build/src/metadata.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file metadata.ts
|
|
3
|
-
* @author tngan
|
|
4
|
-
* @desc An abstraction for metadata of identity provider and service provider
|
|
5
|
-
*/
|
|
2
|
+
* @file metadata.ts
|
|
3
|
+
* @author tngan
|
|
4
|
+
* @desc An abstraction for metadata of identity provider and service provider
|
|
5
|
+
*/
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import { namespace } from './urn.js';
|
|
8
8
|
import { extract } from './extractor.js';
|
|
9
9
|
import { isString } from './utility.js';
|
|
10
10
|
export default class Metadata {
|
|
11
11
|
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
* @param {string | Buffer} xml
|
|
13
|
+
* @param {object} extraParse for custom metadata extractor
|
|
14
|
+
*/
|
|
15
15
|
constructor(xml, extraParse = []) {
|
|
16
16
|
this.xmlString = xml.toString();
|
|
17
17
|
this.meta = extract(this.xmlString, extraParse.concat([
|
|
@@ -66,46 +66,46 @@ export default class Metadata {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
* @desc Get the metadata in xml format
|
|
70
|
+
* @return {string} metadata in xml format
|
|
71
|
+
*/
|
|
72
72
|
getMetadata() {
|
|
73
73
|
return this.xmlString;
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
* @desc Export the metadata to specific file
|
|
77
|
+
* @param {string} exportFile is the output file path
|
|
78
|
+
*/
|
|
79
79
|
exportMetadata(exportFile) {
|
|
80
80
|
fs.writeFileSync(exportFile, this.xmlString);
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
* @desc Get the entityID in metadata
|
|
84
|
+
* @return {string} entityID
|
|
85
|
+
*/
|
|
86
86
|
getEntityID() {
|
|
87
87
|
return this.meta.entityID;
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
* @desc Get the x509 certificate declared in entity metadata
|
|
91
|
+
* @param {string} use declares the type of certificate
|
|
92
|
+
* @return {string} certificate in string format
|
|
93
|
+
*/
|
|
94
94
|
getX509Certificate(use) {
|
|
95
95
|
return this.meta.certificate[use] || null;
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
* @desc Get the support NameID format declared in entity metadata
|
|
99
|
+
* @return {array} support NameID format
|
|
100
|
+
*/
|
|
101
101
|
getNameIDFormat() {
|
|
102
102
|
return this.meta.nameIDFormat;
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
* @desc Get the entity endpoint for single logout service
|
|
106
|
+
* @param {string} binding e.g. redirect, post
|
|
107
|
+
* @return {string/object} location
|
|
108
|
+
*/
|
|
109
109
|
getSingleLogoutService(binding) {
|
|
110
110
|
if (binding && isString(binding)) {
|
|
111
111
|
const bindType = namespace.binding[binding];
|
|
@@ -121,10 +121,31 @@ export default class Metadata {
|
|
|
121
121
|
return this.meta.singleLogoutService;
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
* @desc Get the entity endpoint for single logout service
|
|
125
|
+
* @param {string} binding e.g. redirect, post
|
|
126
|
+
* @return {string/object} location
|
|
127
|
+
*/
|
|
128
|
+
getArtifactResolutionService(binding) {
|
|
129
|
+
if (binding && isString(binding)) {
|
|
130
|
+
const bindType = namespace.binding[binding];
|
|
131
|
+
console.log(this.meta);
|
|
132
|
+
console.log("看一下---------------------");
|
|
133
|
+
let artifactResolutionService = this.meta.artifactResolutionService;
|
|
134
|
+
if (!(artifactResolutionService instanceof Array)) {
|
|
135
|
+
artifactResolutionService = [artifactResolutionService];
|
|
136
|
+
}
|
|
137
|
+
const service = artifactResolutionService.find(obj => obj.binding === bindType);
|
|
138
|
+
if (service) {
|
|
139
|
+
return service.location;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return this.meta.artifactResolutionService;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* @desc Get the support bindings
|
|
146
|
+
* @param {[string]} services
|
|
147
|
+
* @return {[string]} support bindings
|
|
148
|
+
*/
|
|
128
149
|
getSupportBindings(services) {
|
|
129
150
|
let supportBindings = [];
|
|
130
151
|
if (services) {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
|
|
2
|
+
<!-- Schema for the SOAP/1.1 envelope
|
|
3
|
+
|
|
4
|
+
Portions © 2001 DevelopMentor.
|
|
5
|
+
© 2001 W3C (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved.
|
|
6
|
+
|
|
7
|
+
This document is governed by the W3C Software License [1] as described in the FAQ [2].
|
|
8
|
+
[1] http://www.w3.org/Consortium/Legal/copyright-software-19980720
|
|
9
|
+
[2] http://www.w3.org/Consortium/Legal/IPR-FAQ-20000620.html#DTD
|
|
10
|
+
By obtaining, using and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions:
|
|
11
|
+
|
|
12
|
+
Permission to use, copy, modify, and distribute this software and its documentation, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the software and documentation or portions thereof, including modifications, that you make:
|
|
13
|
+
|
|
14
|
+
1. The full text of this NOTICE in a location viewable to users of the redistributed or derivative work.
|
|
15
|
+
|
|
16
|
+
2. Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none exist, a short notice of the following form (hypertext is preferred, text is permitted) should be used within the body of any redistributed or derivative code: "Copyright © 2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/"
|
|
17
|
+
|
|
18
|
+
3. Notice of any changes or modifications to the W3C files, including the date changes were made. (We recommend you provide URIs to the location from which the code is derived.)
|
|
19
|
+
|
|
20
|
+
Original W3C files; http://www.w3.org/2001/06/soap-envelope
|
|
21
|
+
Changes made:
|
|
22
|
+
- reverted namespace to http://schemas.xmlsoap.org/soap/envelope/
|
|
23
|
+
- reverted mustUnderstand to only allow 0 and 1 as lexical values
|
|
24
|
+
- made encodingStyle a global attribute 20020825
|
|
25
|
+
- removed default value from mustUnderstand attribute declaration
|
|
26
|
+
|
|
27
|
+
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
|
|
28
|
+
|
|
29
|
+
COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENTATION.
|
|
30
|
+
|
|
31
|
+
The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the software without specific, written prior permission. Title to copyright in this software and any associated documentation will at all times remain with copyright holders.
|
|
32
|
+
|
|
33
|
+
-->
|
|
34
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://schemas.xmlsoap.org/soap/envelope/" targetNamespace="http://schemas.xmlsoap.org/soap/envelope/">
|
|
35
|
+
<!-- Envelope, header and body -->
|
|
36
|
+
<xs:element name="Envelope" type="tns:Envelope"/>
|
|
37
|
+
<xs:complexType name="Envelope">
|
|
38
|
+
<xs:sequence>
|
|
39
|
+
<xs:element ref="tns:Header" minOccurs="0"/>
|
|
40
|
+
<xs:element ref="tns:Body" minOccurs="1"/>
|
|
41
|
+
<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded" processContents="lax"/>
|
|
42
|
+
</xs:sequence>
|
|
43
|
+
<xs:anyAttribute namespace="##other" processContents="lax"/>
|
|
44
|
+
</xs:complexType>
|
|
45
|
+
<xs:element name="Header" type="tns:Header"/>
|
|
46
|
+
<xs:complexType name="Header">
|
|
47
|
+
<xs:sequence>
|
|
48
|
+
<xs:any namespace="##other" minOccurs="0" maxOccurs="unbounded" processContents="lax"/>
|
|
49
|
+
</xs:sequence>
|
|
50
|
+
<xs:anyAttribute namespace="##other" processContents="lax"/>
|
|
51
|
+
</xs:complexType>
|
|
52
|
+
<xs:element name="Body" type="tns:Body"/>
|
|
53
|
+
<xs:complexType name="Body">
|
|
54
|
+
<xs:sequence>
|
|
55
|
+
<xs:any namespace="##any" minOccurs="0" maxOccurs="unbounded" processContents="lax"/>
|
|
56
|
+
</xs:sequence>
|
|
57
|
+
<xs:anyAttribute namespace="##any" processContents="lax">
|
|
58
|
+
<xs:annotation>
|
|
59
|
+
<xs:documentation> Prose in the spec does not specify that attributes are allowed on the Body element </xs:documentation>
|
|
60
|
+
</xs:annotation>
|
|
61
|
+
</xs:anyAttribute>
|
|
62
|
+
</xs:complexType>
|
|
63
|
+
<!-- Global Attributes. The following attributes are intended to be usable via qualified attribute names on any complex type referencing them. -->
|
|
64
|
+
<xs:attribute name="mustUnderstand">
|
|
65
|
+
<xs:simpleType>
|
|
66
|
+
<xs:restriction base="xs:boolean">
|
|
67
|
+
<xs:pattern value="0|1"/>
|
|
68
|
+
</xs:restriction>
|
|
69
|
+
</xs:simpleType>
|
|
70
|
+
</xs:attribute>
|
|
71
|
+
<xs:attribute name="actor" type="xs:anyURI"/>
|
|
72
|
+
<xs:simpleType name="encodingStyle">
|
|
73
|
+
<xs:annotation>
|
|
74
|
+
<xs:documentation> 'encodingStyle' indicates any canonicalization conventions followed in the contents of the containing element. For example, the value 'http://schemas.xmlsoap.org/soap/encoding/' indicates the pattern described in SOAP specification </xs:documentation>
|
|
75
|
+
</xs:annotation>
|
|
76
|
+
<xs:list itemType="xs:anyURI"/>
|
|
77
|
+
</xs:simpleType>
|
|
78
|
+
<xs:attribute name="encodingStyle" type="tns:encodingStyle"/>
|
|
79
|
+
<xs:attributeGroup name="encodingStyle">
|
|
80
|
+
<xs:attribute ref="tns:encodingStyle"/>
|
|
81
|
+
</xs:attributeGroup>
|
|
82
|
+
<xs:element name="Fault" type="tns:Fault"/>
|
|
83
|
+
<xs:complexType name="Fault" final="extension">
|
|
84
|
+
<xs:annotation>
|
|
85
|
+
<xs:documentation> Fault reporting structure </xs:documentation>
|
|
86
|
+
</xs:annotation>
|
|
87
|
+
<xs:sequence>
|
|
88
|
+
<xs:element name="faultcode" type="xs:QName"/>
|
|
89
|
+
<xs:element name="faultstring" type="xs:string"/>
|
|
90
|
+
<xs:element name="faultactor" type="xs:anyURI" minOccurs="0"/>
|
|
91
|
+
<xs:element name="detail" type="tns:detail" minOccurs="0"/>
|
|
92
|
+
</xs:sequence>
|
|
93
|
+
</xs:complexType>
|
|
94
|
+
<xs:complexType name="detail">
|
|
95
|
+
<xs:sequence>
|
|
96
|
+
<xs:any namespace="##any" minOccurs="0" maxOccurs="unbounded" processContents="lax"/>
|
|
97
|
+
</xs:sequence>
|
|
98
|
+
<xs:anyAttribute namespace="##any" processContents="lax"/>
|
|
99
|
+
</xs:complexType>
|
|
100
|
+
</xs:schema>
|
|
@@ -13,9 +13,37 @@ const schemas = [
|
|
|
13
13
|
'xenc-schema.xsd',
|
|
14
14
|
'saml-schema-metadata-2.0.xsd',
|
|
15
15
|
'saml-schema-ecp-2.0.xsd',
|
|
16
|
-
'saml-schema-dce-2.0.xsd'
|
|
16
|
+
'saml-schema-dce-2.0.xsd',
|
|
17
|
+
'env.xsd'
|
|
17
18
|
];
|
|
19
|
+
function detectXXEIndicators(samlString) {
|
|
20
|
+
const xxePatterns = [
|
|
21
|
+
/<!DOCTYPE\s[^>]*>/i,
|
|
22
|
+
/<!ENTITY\s+[^\s>]+\s+(?:SYSTEM|PUBLIC)\s+['"][^>]*>/i,
|
|
23
|
+
/&[a-zA-Z0-9._-]+;/g,
|
|
24
|
+
/SYSTEM\s*=/i,
|
|
25
|
+
/PUBLIC\s*=/i,
|
|
26
|
+
/file:\/\//,
|
|
27
|
+
/\.dtd['"]?/
|
|
28
|
+
];
|
|
29
|
+
const matches = {};
|
|
30
|
+
xxePatterns.forEach((pattern, index) => {
|
|
31
|
+
const found = samlString.match(pattern);
|
|
32
|
+
if (found) {
|
|
33
|
+
matches[`pattern_${index}`] = {
|
|
34
|
+
pattern: pattern.toString(),
|
|
35
|
+
matches: found
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return Object.keys(matches).length > 0 ? matches : null;
|
|
40
|
+
}
|
|
18
41
|
export const validate = async (xml) => {
|
|
42
|
+
const indicators = detectXXEIndicators(xml);
|
|
43
|
+
if (indicators) {
|
|
44
|
+
console.error('XXE风险特征:', indicators);
|
|
45
|
+
throw new Error('ERR_EXCEPTION_VALIDATE_XML');
|
|
46
|
+
}
|
|
19
47
|
const schemaPath = path.resolve(__dirname, 'schema');
|
|
20
48
|
const [schema, ...preload] = await Promise.all(schemas.map(async (file) => ({
|
|
21
49
|
fileName: file,
|