samlesa 4.3.5 → 4.5.0
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/README.md +11 -8
- package/build/src/artifact.js +55 -0
- package/build/src/binding-artifact.js +477 -363
- package/build/src/binding-post.js +7 -3
- package/build/src/entity-idp.js +51 -3
- package/build/src/entity-sp.js +35 -30
- package/build/src/extractor.js +22 -4
- package/build/src/flow.js +190 -230
- package/build/src/libsamlSoap.js +88 -96
- package/build/src/saml2-enhancements-integration.js +5 -6
- package/build/src/saml2-enhancements.js +14 -15
- package/build/src/soap.js +34 -105
- package/package.json +10 -10
- package/types/src/artifact.d.ts +14 -0
- package/types/src/artifact.d.ts.map +1 -0
- package/types/src/binding-artifact.d.ts +92 -58
- package/types/src/binding-artifact.d.ts.map +1 -1
- package/types/src/binding-post.d.ts +1 -1
- package/types/src/binding-post.d.ts.map +1 -1
- package/types/src/entity-idp.d.ts +42 -2
- package/types/src/entity-idp.d.ts.map +1 -1
- package/types/src/entity-sp.d.ts +16 -17
- package/types/src/entity-sp.d.ts.map +1 -1
- package/types/src/extractor.d.ts.map +1 -1
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/libsamlSoap.d.ts +9 -2
- package/types/src/libsamlSoap.d.ts.map +1 -1
- package/types/src/saml2-enhancements-integration.d.ts.map +1 -1
- package/types/src/saml2-enhancements.d.ts.map +1 -1
- package/types/src/soap.d.ts +5 -25
- package/types/src/soap.d.ts.map +1 -1
- package/types/src/types.d.ts +3 -0
- package/types/src/types.d.ts.map +1 -1
package/build/src/libsamlSoap.js
CHANGED
|
@@ -1,122 +1,114 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { select } from "xpath";
|
|
3
|
-
import { SignedXml } from "xml-crypto-next";
|
|
4
|
-
import fs from "fs";
|
|
5
|
-
import utility, { flattenDeep } from "./utility.js";
|
|
6
|
-
import libsaml from "./libsaml.js";
|
|
7
|
-
import { wording } from "./urn.js";
|
|
1
|
+
import fs from 'fs';
|
|
8
2
|
import { DOMParser } from '@xmldom/xmldom';
|
|
3
|
+
import { select } from 'xpath';
|
|
4
|
+
import { SignedXml } from 'xml-crypto-next';
|
|
5
|
+
import utility, { normalizeCertificates } from './utility.js';
|
|
6
|
+
import libsaml from './libsaml.js';
|
|
7
|
+
import { wording } from './urn.js';
|
|
8
|
+
import { getContext } from './api.js';
|
|
9
9
|
function toNodeArray(result) {
|
|
10
|
-
if (Array.isArray(result))
|
|
10
|
+
if (Array.isArray(result)) {
|
|
11
11
|
return result;
|
|
12
|
-
|
|
12
|
+
}
|
|
13
|
+
if (result != null && typeof result === 'object' && 'nodeType' in result) {
|
|
13
14
|
return [result];
|
|
15
|
+
}
|
|
14
16
|
return [];
|
|
15
17
|
}
|
|
16
18
|
const certUse = wording.certUse;
|
|
17
19
|
const docParser = new DOMParser();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const docParser = new DOMParser();
|
|
22
|
-
let type = '';
|
|
23
|
-
// 为 SOAP 消息定义 XPath
|
|
24
|
-
const artifactResolveXpath = "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='ArtifactResolve']";
|
|
25
|
-
const artifactResponseXpath = "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='ArtifactResponse']";
|
|
26
|
-
// 检测 ArtifactResolve 或 ArtifactResponse 的存在
|
|
27
|
-
// @ts-expect-error
|
|
28
|
-
const artifactResolveNodes = toNodeArray(select(artifactResolveXpath, doc));
|
|
29
|
-
// @ts-expect-error
|
|
30
|
-
const artifactResponseNodes = toNodeArray(select(artifactResponseXpath, doc));
|
|
31
|
-
// 根据消息类型选择合适的 XPath
|
|
32
|
-
let basePath = "";
|
|
33
|
-
if (artifactResolveNodes?.length > 0) {
|
|
34
|
-
type = 'artifactResolve';
|
|
35
|
-
basePath = "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='ArtifactResolve']";
|
|
20
|
+
function resolvePublicCertificate(signatureNode, opts) {
|
|
21
|
+
if (!opts.keyFile && !opts.metadata) {
|
|
22
|
+
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
36
23
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
basePath = "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='ArtifactResponse']";
|
|
24
|
+
if (opts.keyFile) {
|
|
25
|
+
return fs.readFileSync(opts.keyFile);
|
|
40
26
|
}
|
|
41
|
-
|
|
42
|
-
|
|
27
|
+
const certificateNode = toNodeArray(select(".//*[local-name(.)='X509Certificate']", signatureNode));
|
|
28
|
+
const metadataCerts = normalizeCertificates(opts.metadata.getX509Certificate(certUse.signing));
|
|
29
|
+
if (certificateNode.length === 0 && metadataCerts.length === 0) {
|
|
30
|
+
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
43
31
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
32
|
+
if (certificateNode.length > 0) {
|
|
33
|
+
const x509CertificateData = certificateNode[0].firstChild?.nodeValue || '';
|
|
34
|
+
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
35
|
+
if (metadataCerts.length > 0 && !metadataCerts.includes(x509Certificate)) {
|
|
36
|
+
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
37
|
+
}
|
|
38
|
+
return libsaml.getKeyInfo(x509Certificate).getKey();
|
|
51
39
|
}
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
return libsaml.getKeyInfo(metadataCerts[0]).getKey();
|
|
41
|
+
}
|
|
42
|
+
function extractResolvedMessage(rootNode) {
|
|
43
|
+
const resolvedNodes = toNodeArray(select("./*[local-name()='Response' or local-name()='AuthnRequest' or local-name()='LogoutRequest' or local-name()='LogoutResponse']", rootNode));
|
|
44
|
+
if (resolvedNodes.length === 0) {
|
|
45
|
+
return null;
|
|
54
46
|
}
|
|
55
|
-
return
|
|
47
|
+
return resolvedNodes[0].toString();
|
|
56
48
|
}
|
|
57
|
-
function verifySignature(xml,
|
|
58
|
-
|
|
59
|
-
for (const signatureNode of selection) {
|
|
49
|
+
function verifySignature(xml, signatureNodes, opts) {
|
|
50
|
+
for (const signatureNode of signatureNodes) {
|
|
60
51
|
const sig = new SignedXml();
|
|
61
|
-
|
|
62
|
-
sig.signatureAlgorithm = opts.signatureAlgorithm;
|
|
63
|
-
if (!opts.keyFile && !opts.metadata) {
|
|
64
|
-
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
65
|
-
}
|
|
66
|
-
if (opts.keyFile) {
|
|
67
|
-
sig.publicCert = fs.readFileSync(opts.keyFile);
|
|
68
|
-
}
|
|
69
|
-
if (opts.metadata) {
|
|
70
|
-
const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
|
|
71
|
-
// 证书处理逻辑
|
|
72
|
-
let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
|
|
73
|
-
if (Array.isArray(metadataCert)) {
|
|
74
|
-
metadataCert = flattenDeep(metadataCert);
|
|
75
|
-
}
|
|
76
|
-
else if (typeof metadataCert === 'string') {
|
|
77
|
-
metadataCert = [metadataCert];
|
|
78
|
-
}
|
|
79
|
-
metadataCert = metadataCert.map(utility.normalizeCerString);
|
|
80
|
-
// 没有证书的情况
|
|
81
|
-
if (certificateNode.length === 0 && metadataCert.length === 0) {
|
|
82
|
-
throw new Error('NO_SELECTED_CERTIFICATE');
|
|
83
|
-
}
|
|
84
|
-
if (certificateNode.length !== 0) {
|
|
85
|
-
const x509CertificateData = certificateNode[0].firstChild.data;
|
|
86
|
-
const x509Certificate = utility.normalizeCerString(x509CertificateData);
|
|
87
|
-
if (metadataCert.length >= 1 && !metadataCert.includes(x509Certificate)) {
|
|
88
|
-
throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA');
|
|
89
|
-
}
|
|
90
|
-
sig.publicCert = libsaml.getKeyInfo(x509Certificate).getKey();
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
sig.publicCert = libsaml.getKeyInfo(metadataCert[0]).getKey();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
52
|
+
sig.publicCert = resolvePublicCertificate(signatureNode, opts);
|
|
96
53
|
sig.loadSignature(signatureNode);
|
|
97
|
-
verified = sig.checkSignature(xml);
|
|
54
|
+
const verified = sig.checkSignature(xml);
|
|
98
55
|
if (!verified) {
|
|
99
56
|
throw new Error('ERR_FAILED_TO_VERIFY_SIGNATURE');
|
|
100
57
|
}
|
|
101
|
-
|
|
58
|
+
const signedReferences = sig.getSignedReferences();
|
|
59
|
+
if (signedReferences.length < 1) {
|
|
102
60
|
throw new Error('NO_SIGNATURE_REFERENCES');
|
|
103
61
|
}
|
|
104
|
-
const
|
|
105
|
-
const rootNode = docParser.parseFromString(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
case 'ArtifactResolve':
|
|
109
|
-
return [true, rootNode.toString(), false, false];
|
|
110
|
-
case 'ArtifactResponse':
|
|
111
|
-
// @ts-expect-error
|
|
112
|
-
const Response = select("/*[local-name()='ArtifactResponse']/*[local-name()='Response']", rootNode);
|
|
113
|
-
return [true, Response?.[0].toString(), false, false]; // 签名验证成功但未找到断言
|
|
114
|
-
default:
|
|
115
|
-
return [true, null, false, true]; // 签名验证成功但未找到可识别的内容
|
|
62
|
+
const signedXml = signedReferences[0];
|
|
63
|
+
const rootNode = docParser.parseFromString(signedXml, 'application/xml').documentElement;
|
|
64
|
+
if (!rootNode) {
|
|
65
|
+
throw new Error('ERR_INVALID_SOAP_PAYLOAD');
|
|
116
66
|
}
|
|
67
|
+
if (rootNode.localName === 'ArtifactResolve') {
|
|
68
|
+
return {
|
|
69
|
+
verified: true,
|
|
70
|
+
soapContent: xml,
|
|
71
|
+
message: rootNode.toString(),
|
|
72
|
+
type: 'ArtifactResolve',
|
|
73
|
+
resolvedMessage: null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (rootNode.localName === 'ArtifactResponse') {
|
|
77
|
+
return {
|
|
78
|
+
verified: true,
|
|
79
|
+
soapContent: xml,
|
|
80
|
+
message: rootNode.toString(),
|
|
81
|
+
type: 'ArtifactResponse',
|
|
82
|
+
resolvedMessage: extractResolvedMessage(rootNode),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw new Error('ERR_UNSUPPORTED_SOAP_MESSAGE_TYPE');
|
|
87
|
+
}
|
|
88
|
+
async function verifyAndDecryptSoapMessage(xml, opts) {
|
|
89
|
+
const { dom } = getContext();
|
|
90
|
+
const doc = dom.parseFromString(xml, 'application/xml');
|
|
91
|
+
const artifactResolveXpath = "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='ArtifactResolve']";
|
|
92
|
+
const artifactResponseXpath = "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='ArtifactResponse']";
|
|
93
|
+
const artifactResolveNodes = toNodeArray(select(artifactResolveXpath, doc));
|
|
94
|
+
const artifactResponseNodes = toNodeArray(select(artifactResponseXpath, doc));
|
|
95
|
+
let basePath = '';
|
|
96
|
+
if (artifactResolveNodes.length > 0) {
|
|
97
|
+
basePath = artifactResolveXpath;
|
|
98
|
+
}
|
|
99
|
+
else if (artifactResponseNodes.length > 0) {
|
|
100
|
+
basePath = artifactResponseXpath;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
throw new Error('ERR_UNSUPPORTED_SOAP_MESSAGE_TYPE');
|
|
104
|
+
}
|
|
105
|
+
const messageSignatureXpath = `${basePath}/*[local-name(.)='Signature']`;
|
|
106
|
+
const messageSignatureNodes = toNodeArray(select(messageSignatureXpath, doc));
|
|
107
|
+
if (messageSignatureNodes.length === 0) {
|
|
108
|
+
throw new Error('ERR_ZERO_SIGNATURE');
|
|
117
109
|
}
|
|
118
|
-
return
|
|
110
|
+
return verifySignature(xml, messageSignatureNodes, opts);
|
|
119
111
|
}
|
|
120
112
|
export default {
|
|
121
|
-
verifyAndDecryptSoapMessage
|
|
113
|
+
verifyAndDecryptSoapMessage,
|
|
122
114
|
};
|
|
@@ -29,16 +29,15 @@ export function applyAuthnRequestEnhancements(rawSamlRequest, enhancements) {
|
|
|
29
29
|
if (enhancements.providerName) {
|
|
30
30
|
authnRequestElement.setAttribute('ProviderName', enhancements.providerName);
|
|
31
31
|
}
|
|
32
|
-
//
|
|
33
|
-
if (enhancements.scoping) {
|
|
34
|
-
const scopingElement = buildScopingElement(enhancements.scoping, doc);
|
|
35
|
-
authnRequestElement.appendChild(scopingElement);
|
|
36
|
-
}
|
|
37
|
-
// 3. 添加 RequestedAuthnContext 元素
|
|
32
|
+
// AuthnRequestType 要求 RequestedAuthnContext 先于 Scoping
|
|
38
33
|
if (enhancements.requestedAuthnContext) {
|
|
39
34
|
const contextElement = buildRequestedAuthnContextElement(enhancements.requestedAuthnContext, doc);
|
|
40
35
|
authnRequestElement.appendChild(contextElement);
|
|
41
36
|
}
|
|
37
|
+
if (enhancements.scoping) {
|
|
38
|
+
const scopingElement = buildScopingElement(enhancements.scoping, doc);
|
|
39
|
+
authnRequestElement.appendChild(scopingElement);
|
|
40
|
+
}
|
|
42
41
|
return new XMLSerializer().serializeToString(doc);
|
|
43
42
|
}
|
|
44
43
|
/**
|
|
@@ -30,15 +30,7 @@ export function buildScoping(config) {
|
|
|
30
30
|
if (config.proxyCount !== undefined) {
|
|
31
31
|
scoping['samlp:Scoping'][0]._attr.ProxyCount = config.proxyCount.toString();
|
|
32
32
|
}
|
|
33
|
-
//
|
|
34
|
-
if (config.requesterID && config.requesterID.length > 0) {
|
|
35
|
-
config.requesterID.forEach(id => {
|
|
36
|
-
scoping['samlp:Scoping'].push({
|
|
37
|
-
'samlp:RequesterID': [{ _attr: { URI: id } }]
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
// 添加 IDPList 元素
|
|
33
|
+
// SAML Core 要求 IDPList 先于 RequesterID 出现
|
|
42
34
|
if (config.idpList && config.idpList.length > 0) {
|
|
43
35
|
const idpList = { 'samlp:IDPList': [] };
|
|
44
36
|
config.idpList.forEach(entry => {
|
|
@@ -55,6 +47,14 @@ export function buildScoping(config) {
|
|
|
55
47
|
});
|
|
56
48
|
scoping['samlp:Scoping'].push(idpList);
|
|
57
49
|
}
|
|
50
|
+
// RequesterID 的类型是 anyURI,值必须写在元素文本中而不是属性里
|
|
51
|
+
if (config.requesterID && config.requesterID.length > 0) {
|
|
52
|
+
config.requesterID.forEach(id => {
|
|
53
|
+
scoping['samlp:Scoping'].push({
|
|
54
|
+
'samlp:RequesterID': [id]
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
58
|
return scoping;
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
@@ -86,7 +86,7 @@ export function buildRequestedAuthnContext(config) {
|
|
|
86
86
|
if (config.declRefs && config.declRefs.length > 0) {
|
|
87
87
|
config.declRefs.forEach(ref => {
|
|
88
88
|
requestedAuthnContext['samlp:RequestedAuthnContext'].push({
|
|
89
|
-
'saml:AuthnContextDeclRef': [
|
|
89
|
+
'saml:AuthnContextDeclRef': [ref]
|
|
90
90
|
});
|
|
91
91
|
});
|
|
92
92
|
}
|
|
@@ -292,14 +292,13 @@ export function enhanceAuthnRequest(baseAuthnRequest, enhancedConfig) {
|
|
|
292
292
|
}
|
|
293
293
|
authnRequestContent[0]._attr.ProviderName = enhancedConfig.providerName;
|
|
294
294
|
}
|
|
295
|
-
//
|
|
296
|
-
if (enhancedConfig.scoping) {
|
|
297
|
-
authnRequestContent.push(buildScoping(enhancedConfig.scoping));
|
|
298
|
-
}
|
|
299
|
-
// 添加 RequestedAuthnContext 元素
|
|
295
|
+
// AuthnRequestType 的序列顺序为 RequestedAuthnContext 在前、Scoping 在后
|
|
300
296
|
if (enhancedConfig.requestedAuthnContext) {
|
|
301
297
|
authnRequestContent.push(buildRequestedAuthnContext(enhancedConfig.requestedAuthnContext));
|
|
302
298
|
}
|
|
299
|
+
if (enhancedConfig.scoping) {
|
|
300
|
+
authnRequestContent.push(buildScoping(enhancedConfig.scoping));
|
|
301
|
+
}
|
|
303
302
|
return authnRequest;
|
|
304
303
|
}
|
|
305
304
|
/**
|
package/build/src/soap.js
CHANGED
|
@@ -1,144 +1,73 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import https from 'node:https';
|
|
3
|
-
import crypto from "node:crypto";
|
|
4
2
|
import { Builder } from 'xml2js';
|
|
5
3
|
import iconv from 'iconv-lite';
|
|
6
|
-
|
|
4
|
+
import { generateArtifactId, parseArtifact } from './artifact.js';
|
|
7
5
|
const axiosInstance = axios.create({
|
|
8
|
-
|
|
9
|
-
rejectUnauthorized: false // 允许自签名证书
|
|
10
|
-
})
|
|
6
|
+
timeout: 5000,
|
|
11
7
|
});
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
headers: {
|
|
16
|
-
'Content-Type': 'text/xml',
|
|
17
|
-
'SOAPAction': '"ArtifactResolve"'
|
|
18
|
-
},
|
|
19
|
-
timeout: 5000 // 5秒超时
|
|
20
|
-
});
|
|
21
|
-
return response.data;
|
|
8
|
+
function getAxiosErrorPayload(error) {
|
|
9
|
+
if (error?.response?.data) {
|
|
10
|
+
return error.response.data;
|
|
22
11
|
}
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
if (error instanceof Error) {
|
|
13
|
+
return error;
|
|
25
14
|
}
|
|
15
|
+
return new Error('ERR_SOAP_REQUEST_FAILED');
|
|
26
16
|
}
|
|
27
|
-
|
|
17
|
+
async function sendSoapRequest(url, soapRequest, soapAction) {
|
|
28
18
|
try {
|
|
29
19
|
const response = await axiosInstance.post(url, soapRequest, {
|
|
30
20
|
headers: {
|
|
31
|
-
'Content-Type': 'text/xml',
|
|
32
|
-
|
|
21
|
+
'Content-Type': 'text/xml; charset=utf-8',
|
|
22
|
+
SOAPAction: `"${soapAction}"`,
|
|
33
23
|
},
|
|
34
|
-
timeout: 5000 // 5秒超时
|
|
35
24
|
});
|
|
36
25
|
return response.data;
|
|
37
26
|
}
|
|
38
27
|
catch (error) {
|
|
39
|
-
throw error
|
|
28
|
+
throw getAxiosErrorPayload(error);
|
|
40
29
|
}
|
|
41
30
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
31
|
+
export async function sendArtifactResolve(url, soapRequest) {
|
|
32
|
+
return sendSoapRequest(url, soapRequest, 'ArtifactResolve');
|
|
33
|
+
}
|
|
34
|
+
export async function sendArtifactResponse(url, soapRequest) {
|
|
35
|
+
return sendSoapRequest(url, soapRequest, 'ArtifactResponse');
|
|
36
|
+
}
|
|
48
37
|
export function createArt(entityIDString, endpointIndex = 0) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
else {
|
|
55
|
-
// 确保只在非字符串类型上访问 entityMeta
|
|
56
|
-
sourceEntityId = entityIDString.entityMeta.getEntityID();
|
|
57
|
-
}
|
|
58
|
-
// 1. 固定类型代码 (0x0004 - 2字节)
|
|
59
|
-
const typeCode = Buffer.from([0x00, 0x04]);
|
|
60
|
-
// 2. 端点索引 (2字节,大端序)
|
|
61
|
-
if (endpointIndex < 0 || endpointIndex > 65535) {
|
|
62
|
-
throw new Error("Endpoint index must be between 0 and 65535");
|
|
63
|
-
}
|
|
64
|
-
const endpointBuf = Buffer.alloc(2);
|
|
65
|
-
endpointBuf.writeUInt16BE(endpointIndex);
|
|
66
|
-
// 3. Source ID - 实体ID的SHA-1哈希 (20字节)
|
|
67
|
-
const sourceId = crypto
|
|
68
|
-
.createHash("sha1")
|
|
69
|
-
.update(sourceEntityId)
|
|
70
|
-
.digest();
|
|
71
|
-
// 4. Message Handler - 20字节随机值
|
|
72
|
-
const messageHandler = crypto.randomBytes(20);
|
|
73
|
-
// 组合所有组件 (2+2+20+20 = 44字节)
|
|
74
|
-
const artifact = Buffer.concat([
|
|
75
|
-
typeCode,
|
|
76
|
-
endpointBuf,
|
|
77
|
-
sourceId,
|
|
78
|
-
messageHandler,
|
|
79
|
-
]);
|
|
80
|
-
// 返回Base64编码的Artifact
|
|
38
|
+
const sourceEntityId = typeof entityIDString === 'string'
|
|
39
|
+
? entityIDString
|
|
40
|
+
: entityIDString.entityMeta.getEntityID();
|
|
41
|
+
const artifact = generateArtifactId(sourceEntityId, endpointIndex);
|
|
42
|
+
const origin = parseArtifact(artifact);
|
|
81
43
|
return {
|
|
82
|
-
artifact
|
|
44
|
+
artifact,
|
|
83
45
|
origin: {
|
|
84
|
-
typeCode: typeCode
|
|
85
|
-
endpointIndex: endpointIndex,
|
|
86
|
-
sourceId: sourceId
|
|
87
|
-
messageHandle:
|
|
46
|
+
typeCode: origin.typeCode,
|
|
47
|
+
endpointIndex: origin.endpointIndex,
|
|
48
|
+
sourceId: origin.sourceId,
|
|
49
|
+
messageHandle: origin.messageHandle,
|
|
88
50
|
},
|
|
89
51
|
};
|
|
90
52
|
}
|
|
91
|
-
/**
|
|
92
|
-
* @desc generate Art id
|
|
93
|
-
* @param artifact
|
|
94
|
-
*/
|
|
95
53
|
export function parseArt(artifact) {
|
|
96
|
-
|
|
97
|
-
if (Object.prototype.toString.call(artifact) !== '[object String]') {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const decoded = Buffer.from(artifact, 'base64');
|
|
101
|
-
// 确保长度正确(SAML 工件固定为 44 字节)
|
|
102
|
-
if (decoded.length !== 44) {
|
|
103
|
-
throw new Error(`Invalid artifact length: ${decoded.length}, expected 44 bytes`);
|
|
104
|
-
}
|
|
105
|
-
// 读取前 4 字节(TypeCode + EndpointIndex)
|
|
106
|
-
const typeCode = decoded.readUInt16BE(0);
|
|
107
|
-
const endpointIndex = decoded.readUInt16BE(2);
|
|
108
|
-
// 使用 Buffer.from() 替代 slice()
|
|
109
|
-
const sourceId = Buffer.from(decoded.buffer, // 底层 ArrayBuffer
|
|
110
|
-
decoded.byteOffset + 4, // 起始偏移量
|
|
111
|
-
20 // 长度
|
|
112
|
-
).toString('hex');
|
|
113
|
-
const messageHandle = Buffer.from(decoded.buffer, // 底层 ArrayBuffer
|
|
114
|
-
decoded.byteOffset + 24, // 起始偏移量
|
|
115
|
-
20 // 长度
|
|
116
|
-
).toString('hex');
|
|
117
|
-
return { typeCode, endpointIndex, sourceId, messageHandle };
|
|
54
|
+
return parseArtifact(artifact);
|
|
118
55
|
}
|
|
119
|
-
/**
|
|
120
|
-
* 将对象转换为 ISO-8859-1 编码的 XML 字符串
|
|
121
|
-
* @param {Object} data - 要转换的数据对象
|
|
122
|
-
* @returns {Buffer} - ISO-8859-1 编码的 XML 数据 (Buffer)
|
|
123
|
-
*/
|
|
124
56
|
export function encodeXmlToIso88591(data) {
|
|
125
57
|
try {
|
|
126
|
-
// 1. 创建 XML 构建器
|
|
127
58
|
const builder = new Builder({
|
|
128
|
-
headless: false,
|
|
129
|
-
renderOpts: {
|
|
59
|
+
headless: false,
|
|
60
|
+
renderOpts: { pretty: false },
|
|
130
61
|
xmldec: {
|
|
131
62
|
version: '1.0',
|
|
132
63
|
encoding: 'ISO-8859-1',
|
|
133
|
-
standalone: true
|
|
134
|
-
}
|
|
64
|
+
standalone: true,
|
|
65
|
+
},
|
|
135
66
|
});
|
|
136
|
-
// 2. 构建 XML 字符串 (UTF-8 格式)
|
|
137
67
|
const utf8Xml = builder.buildObject(data);
|
|
138
|
-
// 3. 转换为 ISO-8859-1 编码的 Buffer
|
|
139
68
|
return iconv.encode(utf8Xml, 'iso-8859-1');
|
|
140
69
|
}
|
|
141
70
|
catch (error) {
|
|
142
|
-
throw new Error(`XML
|
|
71
|
+
throw new Error(`XML 缂栫爜澶辫触: ${error.message}`);
|
|
143
72
|
}
|
|
144
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "samlesa",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "High-level API for Single Sign On (SAML 2.0) baseed on samlify ",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"docs": "https://saml.veclea.com",
|
|
56
56
|
"license": "MIT",
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@xmldom/xmldom": "^0.9.
|
|
59
|
-
"axios": "^1.
|
|
58
|
+
"@xmldom/xmldom": "^0.9.10",
|
|
59
|
+
"axios": "^1.15.2",
|
|
60
60
|
"camelcase": "^9.0.0",
|
|
61
61
|
"cross-env": "^10.1.0",
|
|
62
62
|
"iconv-lite": "^0.7.2",
|
|
@@ -71,17 +71,17 @@
|
|
|
71
71
|
"xpath": "^0.0.34"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
|
-
"@types/node": "^25.
|
|
74
|
+
"@types/node": "^25.6.0",
|
|
75
75
|
"@types/pako": "2.0.4",
|
|
76
76
|
"@types/uuid": "11.0.0",
|
|
77
|
-
"@vitest/coverage-istanbul": "^4.1.
|
|
78
|
-
"@vitest/coverage-v8": "4.1.
|
|
77
|
+
"@vitest/coverage-istanbul": "^4.1.5",
|
|
78
|
+
"@vitest/coverage-v8": "4.1.5",
|
|
79
79
|
"copyfiles": "^2.4.1",
|
|
80
80
|
"coveralls": "^3.1.1",
|
|
81
|
-
"esbuild": "^0.
|
|
82
|
-
"jsdom": "^29.0.
|
|
81
|
+
"esbuild": "^0.28.0",
|
|
82
|
+
"jsdom": "^29.0.2",
|
|
83
83
|
"timekeeper": "^2.3.1",
|
|
84
|
-
"typescript": "6.0.
|
|
85
|
-
"vitest": "^4.1.
|
|
84
|
+
"typescript": "6.0.3",
|
|
85
|
+
"vitest": "^4.1.5"
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const SAML2_ARTIFACT_TYPE_CODE = 4;
|
|
2
|
+
export declare const SAML2_ARTIFACT_LENGTH = 44;
|
|
3
|
+
export interface ParsedArtifact {
|
|
4
|
+
artifact: string;
|
|
5
|
+
typeCode: number;
|
|
6
|
+
endpointIndex: number;
|
|
7
|
+
sourceId: string;
|
|
8
|
+
messageHandle: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function computeArtifactSourceId(entityId: string): Buffer;
|
|
11
|
+
export declare function generateArtifactId(entityId: string, endpointIndex?: number): string;
|
|
12
|
+
export declare function parseArtifact(artifact: string): ParsedArtifact;
|
|
13
|
+
export declare function validateArtifact(artifact: string, expectedEntityId?: string): ParsedArtifact;
|
|
14
|
+
//# sourceMappingURL=artifact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifact.d.ts","sourceRoot":"","sources":["../../src/artifact.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,wBAAwB,IAAS,CAAC;AAC/C,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,SAAI,GAAG,MAAM,CAsB9E;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAsB9D;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,cAAc,CAS5F"}
|