samlesa 3.4.3 → 4.0.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 +260 -25
- package/build/src/binding-artifact.js +150 -213
- package/build/src/entity-idp.js +2 -1
- package/build/src/entity-sp.js +19 -17
- package/build/src/flow.js +1 -8
- package/build/src/libsaml.js +10 -36
- package/build/src/schemaValidator.js +5 -7
- package/build/src/urn.js +93 -11
- package/build/src/utility.js +203 -3
- package/package.json +17 -4
- package/types/src/binding-artifact.d.ts +53 -17
- package/types/src/binding-artifact.d.ts.map +1 -1
- package/types/src/entity-idp.d.ts.map +1 -1
- package/types/src/entity-sp.d.ts +12 -14
- 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.map +1 -1
- package/types/src/schemaValidator.d.ts.map +1 -1
- package/types/src/urn.d.ts +53 -5
- package/types/src/urn.d.ts.map +1 -1
- package/types/src/utility.d.ts +35 -0
- package/types/src/utility.d.ts.map +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file binding-
|
|
2
|
+
* @file binding-artifact.ts
|
|
3
3
|
* @author tngan
|
|
4
|
-
* @desc Binding-level API
|
|
4
|
+
* @desc Binding-level API for SAML 2.0 Artifact Binding
|
|
5
|
+
* @see https://docs.oasis-open.org/security/saml/v2.0/saml-bind-2.0-os.pdf
|
|
5
6
|
*/
|
|
6
7
|
import { checkStatus } from "./flow.js";
|
|
7
8
|
import { ParserType, StatusCode, wording } from './urn.js';
|
|
@@ -9,23 +10,21 @@ import * as crypto from "node:crypto";
|
|
|
9
10
|
import libsaml from './libsaml.js';
|
|
10
11
|
import libsamlSoap from './libsamlSoap.js';
|
|
11
12
|
import utility, { get } from './utility.js';
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { randomUUID } from 'node:crypto';
|
|
14
14
|
import postBinding from './binding-post.js';
|
|
15
15
|
import { artifactResolveFields, extract, loginRequestFields, loginResponseFields, logoutRequestFields, logoutResponseFields } from "./extractor.js";
|
|
16
16
|
import { verifyTime } from "./validator.js";
|
|
17
17
|
import { sendArtifactResolve } from "./soap.js";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const binding = wording.binding;
|
|
19
|
+
/**
|
|
20
|
+
* Get default extractor fields based on parser type
|
|
21
|
+
*/
|
|
22
22
|
function getDefaultExtractorFields(parserType, assertion) {
|
|
23
23
|
switch (parserType) {
|
|
24
24
|
case ParserType.SAMLRequest:
|
|
25
25
|
return loginRequestFields;
|
|
26
26
|
case ParserType.SAMLResponse:
|
|
27
27
|
if (!assertion) {
|
|
28
|
-
// unexpected hit
|
|
29
28
|
throw new Error('ERR_EMPTY_ASSERTION');
|
|
30
29
|
}
|
|
31
30
|
return loginResponseFields(assertion);
|
|
@@ -37,25 +36,46 @@ function getDefaultExtractorFields(parserType, assertion) {
|
|
|
37
36
|
throw new Error('ERR_UNDEFINED_PARSERTYPE');
|
|
38
37
|
}
|
|
39
38
|
}
|
|
40
|
-
const binding = wording.binding;
|
|
41
39
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* @param
|
|
45
|
-
* @param
|
|
40
|
+
* Generate a SAML 2.0 compliant Artifact ID
|
|
41
|
+
* Format: [TypeCode: 2 bytes] + [EndpointIndex: 2 bytes] + [SourceID: 20 bytes] + [MessageHandle: 20 bytes]
|
|
42
|
+
* @param issuerId The entity ID of the issuing party (IdP)
|
|
43
|
+
* @param endpointIndex The index of the destination endpoint (default is 1 for Artifact Resolution Service)
|
|
44
|
+
* @returns The Base64 encoded Artifact ID string
|
|
45
|
+
*/
|
|
46
|
+
export function generateArtifactId(issuerId, endpointIndex = 1) {
|
|
47
|
+
// SourceID: 20 bytes SHA-1 of issuer ID
|
|
48
|
+
const issuerHash = crypto.createHash('sha1').update(issuerId).digest();
|
|
49
|
+
const sourceId = issuerHash.subarray(0, 20);
|
|
50
|
+
// TypeCode: 0x0004 (SAML 2.0 Artifact Type)
|
|
51
|
+
const typeCode = Buffer.from([0x00, 0x04]);
|
|
52
|
+
// EndpointIndex: 2 bytes Big Endian
|
|
53
|
+
const indexBuffer = Buffer.alloc(2);
|
|
54
|
+
indexBuffer.writeUInt16BE(endpointIndex, 0);
|
|
55
|
+
// MessageHandle: 20 random bytes
|
|
56
|
+
const messageHandle = crypto.randomBytes(20);
|
|
57
|
+
// Concatenate: 2 + 2 + 20 + 20 = 44 bytes
|
|
58
|
+
const artifactBytes = Buffer.concat([typeCode, indexBuffer, sourceId, messageHandle]);
|
|
59
|
+
// Base64 encode
|
|
60
|
+
return artifactBytes.toString('base64');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* @desc Generate a SOAP-encoded login request for Artifact binding
|
|
64
|
+
* @param {string} referenceTagXPath reference uri
|
|
65
|
+
* @param {object} entity object includes both idp and sp
|
|
66
|
+
* @param {function} customTagReplacement used when developers have their own login request template
|
|
67
|
+
* @returns {BindingContext}
|
|
46
68
|
*/
|
|
47
69
|
function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
|
|
48
70
|
const metadata = {
|
|
49
71
|
idp: entity.idp.entityMeta,
|
|
50
72
|
sp: entity.sp.entityMeta,
|
|
51
|
-
inResponse: entity
|
|
52
|
-
relayState: entity
|
|
73
|
+
inResponse: entity.inResponse,
|
|
74
|
+
relayState: entity.relayState
|
|
53
75
|
};
|
|
54
76
|
const spSetting = entity.sp.entitySetting;
|
|
55
77
|
let id = '';
|
|
56
|
-
let
|
|
57
|
-
let soapTemplate = '';
|
|
58
|
-
let Response = '';
|
|
78
|
+
let soapId = spSetting.generateID();
|
|
59
79
|
if (metadata && metadata.idp && metadata.sp) {
|
|
60
80
|
const base = metadata.idp.getSingleSignOnService(binding.post);
|
|
61
81
|
let rawSamlRequest;
|
|
@@ -80,12 +100,13 @@ function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
|
|
|
80
100
|
});
|
|
81
101
|
}
|
|
82
102
|
const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
|
|
103
|
+
let signedAuthnRequest;
|
|
83
104
|
if (metadata.idp.isWantAuthnRequestsSigned()) {
|
|
84
|
-
|
|
105
|
+
signedAuthnRequest = libsaml.constructSAMLSignature({
|
|
85
106
|
referenceTagXPath,
|
|
86
|
-
privateKey,
|
|
107
|
+
privateKey: privateKey,
|
|
87
108
|
privateKeyPass,
|
|
88
|
-
signatureAlgorithm,
|
|
109
|
+
signatureAlgorithm: signatureAlgorithm,
|
|
89
110
|
transformationAlgorithms,
|
|
90
111
|
rawSamlMessage: rawSamlRequest,
|
|
91
112
|
isBase64Output: false,
|
|
@@ -93,35 +114,29 @@ function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
|
|
|
93
114
|
signatureConfig: spSetting.signatureConfig || {
|
|
94
115
|
prefix: 'ds',
|
|
95
116
|
location: {
|
|
96
|
-
reference: "/*[local-name(.)='AuthnRequest']
|
|
117
|
+
reference: "/*[local-name(.)='AuthnRequest']/*[local-name(.)='Issuer']",
|
|
97
118
|
action: 'after'
|
|
98
119
|
},
|
|
99
120
|
}
|
|
100
121
|
});
|
|
101
|
-
soapTemplate = libsaml.replaceTagsByValue(libsaml.defaultArtAuthnRequestTemplate.context, {
|
|
102
|
-
ID: id2,
|
|
103
|
-
IssueInstant: new Date().toISOString(),
|
|
104
|
-
InResponseTo: metadata.inResponse ?? "",
|
|
105
|
-
Issuer: metadata.sp.getEntityID(),
|
|
106
|
-
AuthnRequest: Response
|
|
107
|
-
});
|
|
108
122
|
}
|
|
109
123
|
else {
|
|
110
|
-
|
|
111
|
-
ID: id2,
|
|
112
|
-
IssueInstant: new Date().toISOString(),
|
|
113
|
-
InResponseTo: metadata.inResponse ?? "",
|
|
114
|
-
Issuer: metadata.sp.getEntityID(),
|
|
115
|
-
AuthnRequest: rawSamlRequest
|
|
116
|
-
});
|
|
124
|
+
signedAuthnRequest = rawSamlRequest;
|
|
117
125
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
// Construct SOAP envelope with ArtifactResponse containing AuthnRequest
|
|
127
|
+
const soapTemplate = libsaml.replaceTagsByValue(libsaml.defaultArtAuthnRequestTemplate.context, {
|
|
128
|
+
ID: soapId,
|
|
129
|
+
IssueInstant: new Date().toISOString(),
|
|
130
|
+
InResponseTo: metadata.inResponse ?? "",
|
|
131
|
+
Issuer: metadata.sp.getEntityID(),
|
|
132
|
+
AuthnRequest: signedAuthnRequest
|
|
133
|
+
});
|
|
134
|
+
// Sign the SOAP envelope
|
|
135
|
+
const signedSoap = libsaml.constructSAMLSignature({
|
|
121
136
|
referenceTagXPath: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']",
|
|
122
|
-
privateKey,
|
|
137
|
+
privateKey: privateKey,
|
|
123
138
|
privateKeyPass,
|
|
124
|
-
signatureAlgorithm,
|
|
139
|
+
signatureAlgorithm: signatureAlgorithm,
|
|
125
140
|
transformationAlgorithms,
|
|
126
141
|
rawSamlMessage: soapTemplate,
|
|
127
142
|
isBase64Output: false,
|
|
@@ -135,37 +150,18 @@ function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
|
|
|
135
150
|
}
|
|
136
151
|
},
|
|
137
152
|
});
|
|
153
|
+
return {
|
|
154
|
+
id: soapId,
|
|
155
|
+
context: signedSoap,
|
|
156
|
+
};
|
|
138
157
|
}
|
|
139
158
|
throw new Error('ERR_GENERATE_POST_LOGIN_REQUEST_MISSING_METADATA');
|
|
140
159
|
}
|
|
141
160
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* @
|
|
145
|
-
* @param endpointIndex The index of the destination endpoint (default is '0004' for Artifact Resolution Service).
|
|
146
|
-
* @returns The Base64 encoded Artifact ID string.
|
|
147
|
-
*/
|
|
148
|
-
/**
|
|
149
|
-
* 生成符合 SAML 2.0 规范的 Artifact
|
|
150
|
-
* 结构: [TypeCode: 2 bytes] + [EndpointIndex: 2 bytes] + [SourceID: 20 bytes] + [MessageHandle: 20 bytes]
|
|
161
|
+
* @desc Generate a SOAP-encoded login response for Artifact binding
|
|
162
|
+
* @param {Base64LoginResponseParams} params parameters for generating login response
|
|
163
|
+
* @returns {BindingContext}
|
|
151
164
|
*/
|
|
152
|
-
function generateArtifactId(issuerId, endpointIndex = 1) {
|
|
153
|
-
// 1. SourceID: 20 bytes SHA-1
|
|
154
|
-
const issuerHash = crypto.createHash('sha1').update(issuerId).digest();
|
|
155
|
-
const sourceId = issuerHash.subarray(0, 20); // 必须 20 字节
|
|
156
|
-
// 2. TypeCode: 0x0004
|
|
157
|
-
const typeCode = Buffer.from([0x00, 0x04]);
|
|
158
|
-
// 3. EndpointIndex: 2 bytes Big Endian
|
|
159
|
-
const indexBuffer = Buffer.alloc(2);
|
|
160
|
-
indexBuffer.writeUInt16BE(endpointIndex, 0);
|
|
161
|
-
// 4. MessageHandle: 20 random bytes
|
|
162
|
-
const messageHandle = crypto.randomBytes(20);
|
|
163
|
-
// 5. Concat: 2 + 2 + 20 + 20 = 44 bytes
|
|
164
|
-
const artifactBytes = Buffer.concat([typeCode, indexBuffer, sourceId, messageHandle]);
|
|
165
|
-
// 6. Base64
|
|
166
|
-
return artifactBytes.toString('base64');
|
|
167
|
-
}
|
|
168
|
-
// Initial response that sends only the artifact ID
|
|
169
165
|
async function soapLoginResponse(params) {
|
|
170
166
|
const { entity } = params;
|
|
171
167
|
const metadata = {
|
|
@@ -175,21 +171,12 @@ async function soapLoginResponse(params) {
|
|
|
175
171
|
if (!metadata.idp || !metadata.sp) {
|
|
176
172
|
throw new Error('ERR_GENERATE_ARTIFACT_MISSING_METADATA');
|
|
177
173
|
}
|
|
178
|
-
//
|
|
174
|
+
// Generate the base SAML Response using POST binding logic
|
|
179
175
|
const samlResponseResult = await postBinding.base64LoginResponse(params);
|
|
180
176
|
const samlResponseXml = utility.base64Decode(samlResponseResult.context);
|
|
181
|
-
|
|
182
|
-
console.log("我日你妈==");
|
|
183
|
-
// 2. Generate the SAML 2.0 Artifact ID
|
|
177
|
+
// Generate the SAML 2.0 Artifact ID
|
|
184
178
|
const artifactId = generateArtifactId(metadata.idp.getEntityID());
|
|
185
|
-
//
|
|
186
|
-
// TODO: Implement your Redis caching logic here
|
|
187
|
-
// Example:
|
|
188
|
-
// const redisExpirySeconds = 300; // e.g., expire after 5 minutes
|
|
189
|
-
// await redis.setex(`artifact_cache:${artifactId}`, redisExpirySeconds, samlResponseXml);
|
|
190
|
-
// --- INSERT YOUR REDIS LOGIC HERE ---
|
|
191
|
-
// Example: await cacheInRedis(artifactId, samlResponseXml);
|
|
192
|
-
// 4. Prepare config for SOAP signing (reusing IDP settings)
|
|
179
|
+
// Prepare config for SOAP signing
|
|
193
180
|
const idpSetting = entity.idp.entitySetting;
|
|
194
181
|
const spSetting = entity.sp.entitySetting;
|
|
195
182
|
const config = {
|
|
@@ -199,31 +186,26 @@ async function soapLoginResponse(params) {
|
|
|
199
186
|
signingCert: metadata.idp.getX509Certificate('signing'),
|
|
200
187
|
isBase64Output: false,
|
|
201
188
|
};
|
|
202
|
-
//
|
|
203
|
-
// This is CORRECT - in the initial response we only send the artifact ID
|
|
204
|
-
// The actual SAML response will be retrieved by the SP using the artifact resolution service
|
|
189
|
+
// Construct the SOAP Envelope containing the ArtifactResponse with SAML Response
|
|
205
190
|
const soapBodyContent = `<samlp:ArtifactResponse
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
191
|
+
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
|
192
|
+
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
|
193
|
+
ID="${samlResponseResult.id}_artifact_resp"
|
|
194
|
+
InResponseTo="${params.requestInfo?.extract?.request?.id || ''}"
|
|
195
|
+
Version="2.0"
|
|
196
|
+
IssueInstant="${new Date().toISOString()}">
|
|
197
|
+
<saml2:Issuer>${metadata.idp.getEntityID()}</saml2:Issuer>
|
|
213
198
|
<samlp:Status>
|
|
214
199
|
<samlp:StatusCode Value="${StatusCode.Success}"/>
|
|
215
200
|
</samlp:Status>
|
|
216
|
-
|
|
201
|
+
${samlResponseXml}
|
|
217
202
|
</samlp:ArtifactResponse>`;
|
|
218
203
|
const soapEnvelope = `
|
|
219
204
|
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
|
220
205
|
<soap:Header/>
|
|
221
206
|
<soap:Body>${soapBodyContent}</soap:Body>
|
|
222
207
|
</soap:Envelope>`;
|
|
223
|
-
//
|
|
224
|
-
// Reference the Body element for signature
|
|
225
|
-
// @ts-ignore
|
|
226
|
-
// @ts-ignore
|
|
208
|
+
// Sign the SOAP Envelope
|
|
227
209
|
const signedSoapEnvelope = libsaml.constructSAMLSignature({
|
|
228
210
|
...config,
|
|
229
211
|
rawSamlMessage: soapEnvelope,
|
|
@@ -233,33 +215,40 @@ async function soapLoginResponse(params) {
|
|
|
233
215
|
signatureConfig: {
|
|
234
216
|
prefix: 'ds',
|
|
235
217
|
location: {
|
|
236
|
-
// Place signature inside the ArtifactResponse element, before the Status element
|
|
237
218
|
reference: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']/*[local-name(.)='Status']",
|
|
238
219
|
action: 'before'
|
|
239
220
|
},
|
|
240
221
|
},
|
|
241
222
|
});
|
|
242
|
-
//
|
|
223
|
+
// Return the Artifact ID and the Base64 encoded signed SOAP message
|
|
243
224
|
return {
|
|
244
|
-
id: artifactId,
|
|
245
|
-
context: utility.base64Encode(signedSoapEnvelope),
|
|
225
|
+
id: artifactId,
|
|
226
|
+
context: utility.base64Encode(signedSoapEnvelope),
|
|
246
227
|
};
|
|
247
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* @desc Parse and validate Artifact Resolve request
|
|
231
|
+
* @param {object} params
|
|
232
|
+
* @param {IdentityProvider} params.idp Identity Provider instance
|
|
233
|
+
* @param {ServiceProvider} params.sp Service Provider instance
|
|
234
|
+
* @param {string} params.xml SOAP request XML string
|
|
235
|
+
* @returns {Promise}
|
|
236
|
+
*/
|
|
248
237
|
async function parseLoginRequestResolve(params) {
|
|
249
|
-
|
|
238
|
+
const { idp, sp, xml } = params;
|
|
250
239
|
const verificationOptions = {
|
|
251
240
|
metadata: idp.entityMeta,
|
|
252
241
|
signatureAlgorithm: idp.entitySetting.requestSignatureAlgorithm,
|
|
253
242
|
};
|
|
254
|
-
|
|
243
|
+
// Validate XML
|
|
244
|
+
let res = await libsaml.isValidXml(xml, true).catch(() => {
|
|
255
245
|
return Promise.reject('ERR_EXCEPTION_VALIDATE_XML');
|
|
256
246
|
});
|
|
257
247
|
if (res !== true) {
|
|
258
248
|
return Promise.reject('ERR_EXCEPTION_VALIDATE_XML');
|
|
259
249
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
let [verify, xmlString, isEncrypted, noSignature] = await libsamlSoap.verifyAndDecryptSoapMessage(xml, verificationOptions);
|
|
250
|
+
// Verify and decrypt SOAP message
|
|
251
|
+
let [verify, xmlString] = await libsamlSoap.verifyAndDecryptSoapMessage(xml, verificationOptions);
|
|
263
252
|
if (!verify) {
|
|
264
253
|
return Promise.reject('ERR_FAIL_TO_VERIFY_SIGNATURE');
|
|
265
254
|
}
|
|
@@ -267,25 +256,32 @@ async function parseLoginRequestResolve(params) {
|
|
|
267
256
|
samlContent: xmlString,
|
|
268
257
|
extract: extract(xmlString, artifactResolveFields),
|
|
269
258
|
};
|
|
270
|
-
|
|
271
|
-
* Validation part: validate the context of response after signature is verified and decrypted (optional)
|
|
272
|
-
*/
|
|
259
|
+
// Validation
|
|
273
260
|
const targetEntityMetadata = sp.entityMeta;
|
|
274
261
|
const issuer = targetEntityMetadata.getEntityID();
|
|
275
262
|
const extractedProperties = parseResult.extract;
|
|
276
|
-
//
|
|
263
|
+
// Check issuer
|
|
277
264
|
if (extractedProperties.issuer !== issuer) {
|
|
278
265
|
return Promise.reject('ERR_UNMATCH_ISSUER');
|
|
279
266
|
}
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
267
|
+
// Check time validity (5 minutes from issue instant)
|
|
268
|
+
const issueInstant = new Date(extractedProperties.request.issueInstant);
|
|
269
|
+
const expiryTime = new Date(issueInstant.getTime() + 5 * 60 * 1000);
|
|
270
|
+
if (!verifyTime(undefined, expiryTime.toISOString(), sp.entitySetting.clockDrifts)) {
|
|
283
271
|
return Promise.reject('ERR_EXPIRED_SESSION');
|
|
284
272
|
}
|
|
285
273
|
return Promise.resolve(parseResult);
|
|
286
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* @desc Parse and validate Artifact Resolve response
|
|
277
|
+
* @param {object} params
|
|
278
|
+
* @param {IdentityProvider} params.idp Identity Provider instance
|
|
279
|
+
* @param {ServiceProvider} params.sp Service Provider instance
|
|
280
|
+
* @param {string} params.art Artifact string
|
|
281
|
+
* @returns {Promise}
|
|
282
|
+
*/
|
|
287
283
|
async function parseLoginResponseResolve(params) {
|
|
288
|
-
|
|
284
|
+
const { idp, sp, art } = params;
|
|
289
285
|
const metadata = {
|
|
290
286
|
idp: idp.entityMeta,
|
|
291
287
|
sp: sp.entityMeta,
|
|
@@ -294,14 +290,11 @@ async function parseLoginResponseResolve(params) {
|
|
|
294
290
|
metadata: idp.entityMeta,
|
|
295
291
|
signatureAlgorithm: idp.entitySetting.requestSignatureAlgorithm,
|
|
296
292
|
};
|
|
297
|
-
|
|
298
|
-
/** 断言是否加密应根据响应里面的字段判断*/
|
|
299
|
-
let decryptRequired = idp.entitySetting.isAssertionEncrypted;
|
|
300
|
-
let extractorFields = [];
|
|
301
|
-
let samlContent = '';
|
|
293
|
+
const parserType = ParserType.SAMLResponse;
|
|
302
294
|
const spSetting = sp.entitySetting;
|
|
303
|
-
|
|
304
|
-
|
|
295
|
+
const ID = '_' + randomUUID();
|
|
296
|
+
const url = metadata.idp.getArtifactResolutionService('soap');
|
|
297
|
+
// Construct ArtifactResolve request
|
|
305
298
|
let samlSoapRaw = libsaml.replaceTagsByValue(libsaml.defaultArtifactResolveTemplate.context, {
|
|
306
299
|
ID: ID,
|
|
307
300
|
Destination: url,
|
|
@@ -309,31 +302,36 @@ async function parseLoginResponseResolve(params) {
|
|
|
309
302
|
IssueInstant: new Date().toISOString(),
|
|
310
303
|
Art: art
|
|
311
304
|
});
|
|
305
|
+
let samlContent;
|
|
306
|
+
// Send signed or unsigned ArtifactResolve based on IdP configuration
|
|
312
307
|
if (!metadata.idp.isWantAuthnRequestsSigned()) {
|
|
313
308
|
samlContent = await sendArtifactResolve(url, samlSoapRaw);
|
|
314
|
-
//
|
|
315
|
-
// validate the xml
|
|
309
|
+
// Validate XML
|
|
316
310
|
try {
|
|
317
311
|
await libsaml.isValidXml(samlContent, true);
|
|
318
312
|
}
|
|
319
|
-
catch
|
|
313
|
+
catch {
|
|
320
314
|
return Promise.reject('ERR_INVALID_XML');
|
|
321
315
|
}
|
|
322
316
|
await checkStatus(samlContent, parserType, true);
|
|
317
|
+
// Verify and decrypt SOAP response
|
|
318
|
+
const [verified, verifiedAssertionNode, isDecryptRequired] = await libsamlSoap.verifyAndDecryptSoapMessage(samlContent, verificationOptions);
|
|
319
|
+
if (!verified) {
|
|
320
|
+
return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
|
|
321
|
+
}
|
|
322
|
+
samlContent = verifiedAssertionNode;
|
|
323
323
|
}
|
|
324
|
-
|
|
324
|
+
else {
|
|
325
325
|
const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
// Sign the ArtifactResolve request
|
|
327
|
+
const signatureSoap = libsaml.constructSAMLSignature({
|
|
328
328
|
referenceTagXPath: "//*[local-name(.)='ArtifactResolve']",
|
|
329
329
|
isMessageSigned: false,
|
|
330
330
|
isBase64Output: false,
|
|
331
331
|
transformationAlgorithms: transformationAlgorithms,
|
|
332
|
-
|
|
333
|
-
privateKey,
|
|
332
|
+
privateKey: privateKey,
|
|
334
333
|
privateKeyPass,
|
|
335
|
-
|
|
336
|
-
signatureAlgorithm,
|
|
334
|
+
signatureAlgorithm: signatureAlgorithm,
|
|
337
335
|
rawSamlMessage: samlSoapRaw,
|
|
338
336
|
signingCert: metadata.sp.getX509Certificate('signing'),
|
|
339
337
|
signatureConfig: {
|
|
@@ -345,26 +343,23 @@ async function parseLoginResponseResolve(params) {
|
|
|
345
343
|
}
|
|
346
344
|
});
|
|
347
345
|
samlContent = await sendArtifactResolve(url, signatureSoap);
|
|
348
|
-
//
|
|
349
|
-
// validate the xml
|
|
346
|
+
// Validate XML
|
|
350
347
|
try {
|
|
351
348
|
await libsaml.isValidXml(samlContent, true);
|
|
352
349
|
}
|
|
353
|
-
catch
|
|
350
|
+
catch {
|
|
354
351
|
return Promise.reject('ERR_INVALID_XML');
|
|
355
352
|
}
|
|
356
353
|
await checkStatus(samlContent, parserType, true);
|
|
357
|
-
|
|
358
|
-
|
|
354
|
+
// Verify and decrypt SOAP response
|
|
355
|
+
const [verified1, verifiedAssertionNode1, isDecryptRequired1] = await libsamlSoap.verifyAndDecryptSoapMessage(samlContent, verificationOptions);
|
|
359
356
|
if (!verified1) {
|
|
360
357
|
return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
|
|
361
358
|
}
|
|
362
359
|
samlContent = verifiedAssertionNode1;
|
|
363
|
-
//
|
|
364
|
-
const verificationResult = await libsaml.verifySignature(samlContent, verificationOptions,
|
|
365
|
-
// 检查验证结果
|
|
360
|
+
// Verify SAML signature and decrypt if needed
|
|
361
|
+
const verificationResult = await libsaml.verifySignature(samlContent, verificationOptions, sp);
|
|
366
362
|
if (!verificationResult.status) {
|
|
367
|
-
// 如果验证失败,根据具体情况返回错误
|
|
368
363
|
if (verificationResult.isMessageSigned && !verificationResult.MessageSignatureStatus) {
|
|
369
364
|
return Promise.reject('ERR_FAIL_TO_VERIFY_MESSAGE_SIGNATURE');
|
|
370
365
|
}
|
|
@@ -374,109 +369,51 @@ async function parseLoginResponseResolve(params) {
|
|
|
374
369
|
if (verificationResult.encrypted && !verificationResult.decrypted) {
|
|
375
370
|
return Promise.reject('ERR_FAIL_TO_DECRYPT_ASSERTION');
|
|
376
371
|
}
|
|
377
|
-
// 通用验证失败
|
|
378
372
|
return Promise.reject('ERR_FAIL_TO_VERIFY_SIGNATURE_OR_DECRYPTION');
|
|
379
373
|
}
|
|
380
|
-
// 更新samlContent为验证后的版本(可能已解密)
|
|
381
374
|
samlContent = verificationResult.samlContent;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const parseResult = {
|
|
391
|
-
samlContent: samlContent,
|
|
392
|
-
extract: extract(samlContent, extractorFields),
|
|
393
|
-
};
|
|
394
|
-
/**
|
|
395
|
-
* Validation part: validate the context of response after signature is verified and decrypted (optional)
|
|
396
|
-
*/
|
|
397
|
-
const targetEntityMetadata = idp.entityMeta;
|
|
398
|
-
const issuer = targetEntityMetadata.getEntityID();
|
|
399
|
-
const extractedProperties = parseResult.extract;
|
|
400
|
-
// unmatched issuer
|
|
401
|
-
if ((parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
|
|
402
|
-
&& extractedProperties
|
|
403
|
-
&& extractedProperties.issuer !== issuer) {
|
|
404
|
-
return Promise.reject('ERR_UNMATCH_ISSUER');
|
|
405
|
-
}
|
|
406
|
-
// invalid session time
|
|
407
|
-
// only run the verifyTime when `SessionNotOnOrAfter` exists
|
|
408
|
-
if (parserType === 'SAMLResponse'
|
|
409
|
-
&& extractedProperties.sessionIndex.sessionNotOnOrAfter
|
|
410
|
-
&& !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, sp.entitySetting.clockDrifts)) {
|
|
411
|
-
return Promise.reject('ERR_EXPIRED_SESSION');
|
|
412
|
-
}
|
|
413
|
-
// invalid time
|
|
414
|
-
// 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
|
415
|
-
if (parserType === 'SAMLResponse'
|
|
416
|
-
&& extractedProperties.conditions
|
|
417
|
-
&& !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, sp.entitySetting.clockDrifts)) {
|
|
418
|
-
return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
|
|
419
|
-
}
|
|
420
|
-
return Promise.resolve(parseResult);
|
|
375
|
+
}
|
|
376
|
+
// Extract fields
|
|
377
|
+
let extractorFields;
|
|
378
|
+
if (parserType === 'SAMLResponse') {
|
|
379
|
+
extractorFields = getDefaultExtractorFields(parserType, samlContent);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
extractorFields = getDefaultExtractorFields(parserType, null);
|
|
421
383
|
}
|
|
422
384
|
const parseResult = {
|
|
423
385
|
samlContent: samlContent,
|
|
424
386
|
extract: extract(samlContent, extractorFields),
|
|
425
387
|
};
|
|
426
|
-
|
|
427
|
-
* Validation part: validate the context of response after signature is verified and decrypted (optional)
|
|
428
|
-
*/
|
|
388
|
+
// Validation
|
|
429
389
|
const targetEntityMetadata = idp.entityMeta;
|
|
430
390
|
const issuer = targetEntityMetadata.getEntityID();
|
|
431
391
|
const extractedProperties = parseResult.extract;
|
|
432
|
-
//
|
|
433
|
-
if (
|
|
392
|
+
// Check issuer
|
|
393
|
+
if (parserType === ParserType.SAMLResponse
|
|
434
394
|
&& extractedProperties
|
|
435
395
|
&& extractedProperties.issuer !== issuer) {
|
|
436
396
|
return Promise.reject('ERR_UNMATCH_ISSUER');
|
|
437
397
|
}
|
|
438
|
-
//
|
|
439
|
-
// only run the verifyTime when `SessionNotOnOrAfter` exists
|
|
398
|
+
// Check session time
|
|
440
399
|
if (parserType === 'SAMLResponse'
|
|
441
400
|
&& extractedProperties.sessionIndex.sessionNotOnOrAfter
|
|
442
401
|
&& !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, sp.entitySetting.clockDrifts)) {
|
|
443
402
|
return Promise.reject('ERR_EXPIRED_SESSION');
|
|
444
403
|
}
|
|
445
|
-
//
|
|
446
|
-
// 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
|
404
|
+
// Check conditions time
|
|
447
405
|
if (parserType === 'SAMLResponse'
|
|
448
406
|
&& extractedProperties.conditions
|
|
449
407
|
&& !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, sp.entitySetting.clockDrifts)) {
|
|
450
408
|
return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
|
|
451
409
|
}
|
|
452
|
-
//valid destination
|
|
453
|
-
//There is no validation of the response here. The upper-layer application
|
|
454
|
-
// should verify the result by itself to see if the destination is equal to the SP acs and
|
|
455
|
-
// whether the response.id is used to prevent replay attacks.
|
|
456
|
-
/*
|
|
457
|
-
let destination = extractedProperties?.response?.destination
|
|
458
|
-
let isExit = self.entitySetting?.assertionConsumerService?.filter((item) => {
|
|
459
|
-
return item?.Location === destination
|
|
460
|
-
})
|
|
461
|
-
if (isExit?.length === 0) {
|
|
462
|
-
return Promise.reject('ERR_Destination_URL');
|
|
463
|
-
}
|
|
464
|
-
if (parserType === 'SAMLResponse') {
|
|
465
|
-
let destination = extractedProperties?.response?.destination
|
|
466
|
-
let isExit = self.entitySetting?.assertionConsumerService?.filter((item: { Location: any; }) => {
|
|
467
|
-
return item?.Location === destination
|
|
468
|
-
})
|
|
469
|
-
if (isExit?.length === 0) {
|
|
470
|
-
return Promise.reject('ERR_Destination_URL');
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
*/
|
|
474
410
|
return Promise.resolve(parseResult);
|
|
475
411
|
}
|
|
476
|
-
const
|
|
477
|
-
parseLoginRequestResolve,
|
|
412
|
+
const artifactBinding = {
|
|
478
413
|
soapLoginRequest,
|
|
479
|
-
parseLoginResponseResolve,
|
|
480
414
|
soapLoginResponse,
|
|
415
|
+
parseLoginRequestResolve,
|
|
416
|
+
parseLoginResponseResolve,
|
|
417
|
+
generateArtifactId,
|
|
481
418
|
};
|
|
482
|
-
export default
|
|
419
|
+
export default artifactBinding;
|
package/build/src/entity-idp.js
CHANGED
|
@@ -68,7 +68,7 @@ export class IdentityProvider extends Entity {
|
|
|
68
68
|
sp,
|
|
69
69
|
}, user, relayState, customTagReplacement, AttributeStatement);
|
|
70
70
|
case namespace.binding.artifact:
|
|
71
|
-
|
|
71
|
+
context = await artifactBinding.soapLoginResponse({
|
|
72
72
|
requestInfo,
|
|
73
73
|
entity: {
|
|
74
74
|
idp: this,
|
|
@@ -80,6 +80,7 @@ export class IdentityProvider extends Entity {
|
|
|
80
80
|
AttributeStatement,
|
|
81
81
|
idpInit,
|
|
82
82
|
});
|
|
83
|
+
break;
|
|
83
84
|
default:
|
|
84
85
|
throw new Error('ERR_CREATE_RESPONSE_UNDEFINED_BINDING');
|
|
85
86
|
}
|