samlesa 4.3.5 → 4.4.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 +19 -16
- 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 +21 -4
- package/build/src/libsamlSoap.js +88 -96
- package/build/src/soap.js +34 -105
- package/package.json +87 -87
- 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/soap.d.ts +5 -25
- package/types/src/soap.d.ts.map +1 -1
- package/types/src/types.d.ts +1 -0
- package/types/src/types.d.ts.map +1 -1
|
@@ -87,7 +87,7 @@ function base64LoginRequest(referenceTagXPath, entity, customTagReplacement) {
|
|
|
87
87
|
* @param AttributeStatement
|
|
88
88
|
* @param idpInit
|
|
89
89
|
*/
|
|
90
|
-
async function base64LoginResponse({ requestInfo = {}, entity, user = {}, customTagReplacement, encryptThenSign = false, AttributeStatement = [], idpInit }) {
|
|
90
|
+
async function base64LoginResponse({ requestInfo = {}, entity, user = {}, customTagReplacement, encryptThenSign = false, AttributeStatement = [], idpInit, destinationBinding = binding.post, }) {
|
|
91
91
|
const idpSetting = entity.idp.entitySetting;
|
|
92
92
|
const spSetting = entity.sp.entitySetting;
|
|
93
93
|
// @ts-ignore
|
|
@@ -100,7 +100,9 @@ async function base64LoginResponse({ requestInfo = {}, entity, user = {}, custom
|
|
|
100
100
|
const nameIDFormat = idpSetting.nameIDFormat;
|
|
101
101
|
const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : namespace.format.unspecified;
|
|
102
102
|
if (metadata && metadata.idp && metadata.sp) {
|
|
103
|
-
const base =
|
|
103
|
+
const base = destinationBinding === binding.artifact
|
|
104
|
+
? metadata.sp.getAssertionConsumerService(binding.artifact, { mode: 'lenient' })
|
|
105
|
+
: metadata.sp.getAssertionConsumerService(binding.post);
|
|
104
106
|
let rawSamlResponse;
|
|
105
107
|
const nowTime = new Date();
|
|
106
108
|
const spEntityID = metadata.sp.getEntityID();
|
|
@@ -108,7 +110,9 @@ async function base64LoginResponse({ requestInfo = {}, entity, user = {}, custom
|
|
|
108
110
|
oneMinutesLaterTime.setMinutes(oneMinutesLaterTime.getMinutes() + 5);
|
|
109
111
|
const OneMinutesLater = oneMinutesLaterTime.toISOString();
|
|
110
112
|
const now = nowTime.toISOString();
|
|
111
|
-
const acl =
|
|
113
|
+
const acl = destinationBinding === binding.artifact
|
|
114
|
+
? metadata.sp.getAssertionConsumerService(binding.artifact, { mode: 'lenient' })
|
|
115
|
+
: metadata.sp.getAssertionConsumerService(binding.post);
|
|
112
116
|
// @ts-ignore
|
|
113
117
|
const sessionID = idpSetting?.generateID() ?? `_${randomUUID()}`;
|
|
114
118
|
const sessionIndex = 'session' + sessionID; // 这个是当前系统的会话索引,用于单点注销
|
package/build/src/entity-idp.js
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
* @author tngan
|
|
4
4
|
* @desc Declares the actions taken by identity provider
|
|
5
5
|
*/
|
|
6
|
-
import { wording, } from './urn.js';
|
|
7
|
-
const binding = wording.binding;
|
|
8
6
|
import Entity from './entity.js';
|
|
9
7
|
import { namespace } from './urn.js';
|
|
10
8
|
import postBinding from './binding-post.js';
|
|
@@ -68,7 +66,7 @@ export class IdentityProvider extends Entity {
|
|
|
68
66
|
sp,
|
|
69
67
|
}, user, relayState, customTagReplacement, AttributeStatement);
|
|
70
68
|
case namespace.binding.artifact:
|
|
71
|
-
context = await artifactBinding.
|
|
69
|
+
context = await artifactBinding.createLoginResponse({
|
|
72
70
|
requestInfo,
|
|
73
71
|
entity: {
|
|
74
72
|
idp: this,
|
|
@@ -91,6 +89,49 @@ export class IdentityProvider extends Entity {
|
|
|
91
89
|
type: 'SAMLResponse'
|
|
92
90
|
};
|
|
93
91
|
}
|
|
92
|
+
createArtifactResolveRequest(sp, artifact) {
|
|
93
|
+
return artifactBinding.createArtifactResolveRequest({
|
|
94
|
+
requester: this,
|
|
95
|
+
responder: sp,
|
|
96
|
+
artifact,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async createArtifactResolveResponse(params) {
|
|
100
|
+
const { sp, samlMessage, requestInfo = {}, user = {}, customTagReplacement, encryptThenSign = false, AttributeStatement = [], idpInit = false, inResponseTo = '', } = params;
|
|
101
|
+
const resolvedMessage = samlMessage ?? (await artifactBinding.createLoginResponse({
|
|
102
|
+
requestInfo,
|
|
103
|
+
entity: {
|
|
104
|
+
idp: this,
|
|
105
|
+
sp,
|
|
106
|
+
},
|
|
107
|
+
user,
|
|
108
|
+
customTagReplacement,
|
|
109
|
+
encryptThenSign,
|
|
110
|
+
AttributeStatement,
|
|
111
|
+
idpInit,
|
|
112
|
+
})).samlContent;
|
|
113
|
+
return artifactBinding.createArtifactResolveResponse({
|
|
114
|
+
requester: sp,
|
|
115
|
+
responder: this,
|
|
116
|
+
inResponseTo,
|
|
117
|
+
samlMessage: resolvedMessage,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
parseArtifactResolveRequest(sp, xml) {
|
|
121
|
+
return artifactBinding.parseArtifactResolveRequest({
|
|
122
|
+
requester: sp,
|
|
123
|
+
responder: this,
|
|
124
|
+
xml,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
parseArtifactResolveResponse(sp, xml, inResponseTo) {
|
|
128
|
+
return artifactBinding.parseArtifactResolveResponse({
|
|
129
|
+
requester: this,
|
|
130
|
+
responder: sp,
|
|
131
|
+
xml,
|
|
132
|
+
inResponseTo,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
94
135
|
/**
|
|
95
136
|
* Validation of the parsed URL parameters
|
|
96
137
|
* @param sp ServiceProvider instance
|
|
@@ -98,6 +139,13 @@ export class IdentityProvider extends Entity {
|
|
|
98
139
|
* @param req RequesmessageSigningOrderst
|
|
99
140
|
*/
|
|
100
141
|
parseLoginRequest(sp, binding, req) {
|
|
142
|
+
if (binding === namespace.binding.artifact || binding === 'artifact') {
|
|
143
|
+
return artifactBinding.parseLoginRequest({
|
|
144
|
+
idp: this,
|
|
145
|
+
sp,
|
|
146
|
+
request: req,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
101
149
|
const self = this;
|
|
102
150
|
return flow({
|
|
103
151
|
from: sp,
|
package/build/src/entity-sp.js
CHANGED
|
@@ -61,7 +61,7 @@ export class ServiceProvider extends Entity {
|
|
|
61
61
|
context = simpleSignBinding.base64LoginRequest({ idp, sp: this }, customTagReplacement);
|
|
62
62
|
break;
|
|
63
63
|
case nsBinding.artifact:
|
|
64
|
-
context = artifactBinding.
|
|
64
|
+
context = artifactBinding.createLoginRequest("/*[local-name(.)='AuthnRequest']", {
|
|
65
65
|
idp,
|
|
66
66
|
sp: this
|
|
67
67
|
}, customTagReplacement);
|
|
@@ -76,14 +76,24 @@ export class ServiceProvider extends Entity {
|
|
|
76
76
|
type: 'SAMLRequest',
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
createArtifactResolveRequest(idp, artifact) {
|
|
80
|
+
return artifactBinding.createArtifactResolveRequest({
|
|
81
|
+
requester: this,
|
|
82
|
+
responder: idp,
|
|
83
|
+
artifact,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async createArtifactResolveResponse(idp, config) {
|
|
87
|
+
const samlMessage = config?.samlMessage ?? artifactBinding.createLoginRequest("/*[local-name(.)='AuthnRequest']", {
|
|
81
88
|
idp,
|
|
82
89
|
sp: this,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
}, config?.customTagReplacement).samlContent;
|
|
91
|
+
return artifactBinding.createArtifactResolveResponse({
|
|
92
|
+
requester: idp,
|
|
93
|
+
responder: this,
|
|
94
|
+
inResponseTo: config?.inResponseTo || '',
|
|
95
|
+
samlMessage,
|
|
96
|
+
});
|
|
87
97
|
}
|
|
88
98
|
/**
|
|
89
99
|
* @desc Validation of the parsed the URL parameters
|
|
@@ -92,6 +102,13 @@ export class ServiceProvider extends Entity {
|
|
|
92
102
|
* @param {request} req request
|
|
93
103
|
*/
|
|
94
104
|
parseLoginResponse(idp, binding, request) {
|
|
105
|
+
if (binding === namespace.binding.artifact || binding === 'artifact') {
|
|
106
|
+
return artifactBinding.parseLoginResponse({
|
|
107
|
+
idp,
|
|
108
|
+
sp: this,
|
|
109
|
+
request,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
95
112
|
const self = this;
|
|
96
113
|
return flow({
|
|
97
114
|
from: idp,
|
|
@@ -103,31 +120,19 @@ export class ServiceProvider extends Entity {
|
|
|
103
120
|
request: request
|
|
104
121
|
});
|
|
105
122
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
parseLoginRequestResolve(idp, xml) {
|
|
112
|
-
const self = this;
|
|
113
|
-
return artifactBinding.parseLoginRequestResolve({
|
|
114
|
-
idp: idp,
|
|
115
|
-
sp: self,
|
|
116
|
-
xml: xml
|
|
123
|
+
parseArtifactResolveRequest(idp, xml) {
|
|
124
|
+
return artifactBinding.parseArtifactResolveRequest({
|
|
125
|
+
requester: idp,
|
|
126
|
+
responder: this,
|
|
127
|
+
xml,
|
|
117
128
|
});
|
|
118
129
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
parseLoginResponseResolve(idp, art, request) {
|
|
126
|
-
const self = this;
|
|
127
|
-
return artifactBinding.parseLoginResponseResolve({
|
|
128
|
-
idp: idp,
|
|
129
|
-
sp: self,
|
|
130
|
-
art: art
|
|
130
|
+
parseArtifactResolveResponse(idp, xml, inResponseTo) {
|
|
131
|
+
return artifactBinding.parseArtifactResolveResponse({
|
|
132
|
+
requester: this,
|
|
133
|
+
responder: idp,
|
|
134
|
+
xml,
|
|
135
|
+
inResponseTo,
|
|
131
136
|
});
|
|
132
137
|
}
|
|
133
138
|
}
|
package/build/src/extractor.js
CHANGED
|
@@ -140,10 +140,27 @@ export const artifactResolveFields = [
|
|
|
140
140
|
},
|
|
141
141
|
];
|
|
142
142
|
export const artifactResponseFields = [
|
|
143
|
-
{
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
{
|
|
144
|
+
key: 'response',
|
|
145
|
+
localPath: ['Envelope', 'Body', 'ArtifactResponse'],
|
|
146
|
+
attributes: ['ID', 'IssueInstant', 'Version', 'InResponseTo', 'Destination']
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
key: 'issuer',
|
|
150
|
+
localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Issuer'],
|
|
151
|
+
attributes: []
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
key: 'status',
|
|
155
|
+
localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Status', 'StatusCode'],
|
|
156
|
+
attributes: ['Value']
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
key: 'signature',
|
|
160
|
+
localPath: ['Envelope', 'Body', 'ArtifactResponse', 'Signature'],
|
|
161
|
+
attributes: [],
|
|
162
|
+
context: true
|
|
163
|
+
},
|
|
147
164
|
];
|
|
148
165
|
export const loginResponseStatusFields = [
|
|
149
166
|
{ key: 'top', localPath: ['Response', 'Status', 'StatusCode'], attributes: ['Value'] },
|
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
|
};
|
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
|
}
|