samlesa 3.4.2 → 3.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.
@@ -1,29 +1,30 @@
1
1
  /**
2
- * @file binding-post.ts
2
+ * @file binding-artifact.ts
3
3
  * @author tngan
4
- * @desc Binding-level API, declare the functions using POST binding
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
+ import * as crypto from "node:crypto";
8
10
  import libsaml from './libsaml.js';
9
11
  import libsamlSoap from './libsamlSoap.js';
10
12
  import utility, { get } from './utility.js';
11
- import { fileURLToPath } from "node:url";
12
13
  import { randomUUID } from 'node:crypto';
14
+ import postBinding from './binding-post.js';
13
15
  import { artifactResolveFields, extract, loginRequestFields, loginResponseFields, logoutRequestFields, logoutResponseFields } from "./extractor.js";
14
16
  import { verifyTime } from "./validator.js";
15
17
  import { sendArtifactResolve } from "./soap.js";
16
- import path from "node:path";
17
- const __filename = fileURLToPath(import.meta.url);
18
- const __dirname = path.dirname(__filename);
19
- // get the default extractor fields based on the parserType
18
+ const binding = wording.binding;
19
+ /**
20
+ * Get default extractor fields based on parser type
21
+ */
20
22
  function getDefaultExtractorFields(parserType, assertion) {
21
23
  switch (parserType) {
22
24
  case ParserType.SAMLRequest:
23
25
  return loginRequestFields;
24
26
  case ParserType.SAMLResponse:
25
27
  if (!assertion) {
26
- // unexpected hit
27
28
  throw new Error('ERR_EMPTY_ASSERTION');
28
29
  }
29
30
  return loginResponseFields(assertion);
@@ -35,25 +36,46 @@ function getDefaultExtractorFields(parserType, assertion) {
35
36
  throw new Error('ERR_UNDEFINED_PARSERTYPE');
36
37
  }
37
38
  }
38
- const binding = wording.binding;
39
39
  /**
40
- * @desc Generate a base64 encoded login request
41
- * @param {string} referenceTagXPath reference uri
42
- * @param {object} entity object includes both idp and sp
43
- * @param customTagReplacement
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}
44
68
  */
45
69
  function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
46
70
  const metadata = {
47
71
  idp: entity.idp.entityMeta,
48
72
  sp: entity.sp.entityMeta,
49
- inResponse: entity?.inResponse,
50
- relayState: entity?.relayState
73
+ inResponse: entity.inResponse,
74
+ relayState: entity.relayState
51
75
  };
52
76
  const spSetting = entity.sp.entitySetting;
53
77
  let id = '';
54
- let id2 = spSetting.generateID();
55
- let soapTemplate = '';
56
- let Response = '';
78
+ let soapId = spSetting.generateID();
57
79
  if (metadata && metadata.idp && metadata.sp) {
58
80
  const base = metadata.idp.getSingleSignOnService(binding.post);
59
81
  let rawSamlRequest;
@@ -78,12 +100,13 @@ function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
78
100
  });
79
101
  }
80
102
  const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
103
+ let signedAuthnRequest;
81
104
  if (metadata.idp.isWantAuthnRequestsSigned()) {
82
- Response = libsaml.constructSAMLSignature({
105
+ signedAuthnRequest = libsaml.constructSAMLSignature({
83
106
  referenceTagXPath,
84
- privateKey,
107
+ privateKey: privateKey,
85
108
  privateKeyPass,
86
- signatureAlgorithm,
109
+ signatureAlgorithm: signatureAlgorithm,
87
110
  transformationAlgorithms,
88
111
  rawSamlMessage: rawSamlRequest,
89
112
  isBase64Output: false,
@@ -91,35 +114,29 @@ function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
91
114
  signatureConfig: spSetting.signatureConfig || {
92
115
  prefix: 'ds',
93
116
  location: {
94
- reference: "/*[local-name(.)='AuthnRequest']/!*[local-name(.)='Issuer']",
117
+ reference: "/*[local-name(.)='AuthnRequest']/*[local-name(.)='Issuer']",
95
118
  action: 'after'
96
119
  },
97
120
  }
98
121
  });
99
- soapTemplate = libsaml.replaceTagsByValue(libsaml.defaultArtAuthnRequestTemplate.context, {
100
- ID: id2,
101
- IssueInstant: new Date().toISOString(),
102
- InResponseTo: metadata.inResponse ?? "",
103
- Issuer: metadata.sp.getEntityID(),
104
- AuthnRequest: Response
105
- });
106
122
  }
107
123
  else {
108
- soapTemplate = libsaml.replaceTagsByValue(libsaml.defaultArtAuthnRequestTemplate.context, {
109
- ID: id2,
110
- IssueInstant: new Date().toISOString(),
111
- InResponseTo: metadata.inResponse ?? "",
112
- Issuer: metadata.sp.getEntityID(),
113
- AuthnRequest: rawSamlRequest
114
- });
124
+ signedAuthnRequest = rawSamlRequest;
115
125
  }
116
- /** 构建响应签名*/
117
- // No need to embeded XML signature
118
- return libsaml.constructSAMLSignature({
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({
119
136
  referenceTagXPath: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']",
120
- privateKey,
137
+ privateKey: privateKey,
121
138
  privateKeyPass,
122
- signatureAlgorithm,
139
+ signatureAlgorithm: signatureAlgorithm,
123
140
  transformationAlgorithms,
124
141
  rawSamlMessage: soapTemplate,
125
142
  isBase64Output: false,
@@ -133,161 +150,105 @@ function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
133
150
  }
134
151
  },
135
152
  });
153
+ return {
154
+ id: soapId,
155
+ context: signedSoap,
156
+ };
136
157
  }
137
158
  throw new Error('ERR_GENERATE_POST_LOGIN_REQUEST_MISSING_METADATA');
138
159
  }
139
160
  /**
140
- * @desc Generate a base64 encoded login response
141
- * @param {object} requestInfo corresponding request, used to obtain the id
142
- * @param {object} entity object includes both idp and sp
143
- * @param {object} user current logged user (e.g. req.user)
144
- * @param {function} customTagReplacement used when developers have their own login response template
145
- * @param {boolean} encryptThenSign whether or not to encrypt then sign first (if signing). Defaults to sign-then-encrypt
146
- * @param AttributeStatement
161
+ * @desc Generate a SOAP-encoded login response for Artifact binding
162
+ * @param {Base64LoginResponseParams} params parameters for generating login response
163
+ * @returns {BindingContext}
147
164
  */
148
- async function soapLoginResponse(requestInfo = {}, entity, user = {}, customTagReplacement, encryptThenSign = false, AttributeStatement = []) {
149
- const idpSetting = entity.idp.entitySetting;
150
- const spSetting = entity.sp.entitySetting;
151
- const id = idpSetting.generateID();
165
+ async function soapLoginResponse(params) {
166
+ const { entity } = params;
152
167
  const metadata = {
153
168
  idp: entity.idp.entityMeta,
154
169
  sp: entity.sp.entityMeta,
155
170
  };
156
- const nameIDFormat = idpSetting.nameIDFormat;
157
- const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat;
158
- if (metadata && metadata.idp && metadata.sp) {
159
- const base = metadata.sp.getAssertionConsumerService(binding.post);
160
- let rawSamlResponse;
161
- const nowTime = new Date();
162
- const spEntityID = metadata.sp.getEntityID();
163
- const oneMinutesLaterTime = new Date(nowTime.getTime());
164
- oneMinutesLaterTime.setMinutes(oneMinutesLaterTime.getMinutes() + 5);
165
- const OneMinutesLater = oneMinutesLaterTime.toISOString();
166
- const now = nowTime.toISOString();
167
- const acl = metadata.sp.getAssertionConsumerService(binding.post);
168
- const sessionIndex = 'session' + idpSetting.generateID(); // 这个是当前系统的会话索引,用于单点注销
169
- const tenHoursLaterTime = new Date(nowTime.getTime());
170
- tenHoursLaterTime.setHours(tenHoursLaterTime.getHours() + 10);
171
- const tenHoursLater = tenHoursLaterTime.toISOString();
172
- const tvalue = {
173
- ID: id,
174
- AssertionID: idpSetting.generateID(),
175
- Destination: base,
176
- Audience: spEntityID,
177
- EntityID: spEntityID,
178
- SubjectRecipient: acl,
179
- Issuer: metadata.idp.getEntityID(),
180
- IssueInstant: now,
181
- AssertionConsumerServiceURL: acl,
182
- StatusCode: StatusCode.Success,
183
- // can be customized
184
- ConditionsNotBefore: now,
185
- ConditionsNotOnOrAfter: OneMinutesLater,
186
- SubjectConfirmationDataNotOnOrAfter: OneMinutesLater,
187
- NameIDFormat: selectedNameIDFormat,
188
- NameID: user?.NameID || '',
189
- InResponseTo: get(requestInfo, 'extract.request.id', ''),
190
- AuthnStatement: `<saml:AuthnStatement AuthnInstant="${now}" SessionNotOnOrAfter="${tenHoursLater}" SessionIndex="${sessionIndex}"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement>`,
191
- AttributeStatement: libsaml.attributeStatementBuilder(AttributeStatement),
192
- };
193
- if (idpSetting.loginResponseTemplate && customTagReplacement) {
194
- const template = customTagReplacement(idpSetting.loginResponseTemplate.context);
195
- rawSamlResponse = get(template, 'context', null);
196
- }
197
- else {
198
- if (requestInfo !== null) {
199
- tvalue.InResponseTo = requestInfo?.extract?.request?.id ?? '';
200
- }
201
- rawSamlResponse = libsaml.replaceTagsByValue(libsaml.defaultLoginResponseTemplate.context, tvalue);
202
- }
203
- const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm } = idpSetting;
204
- const config = {
205
- privateKey,
206
- privateKeyPass,
207
- signatureAlgorithm,
208
- signingCert: metadata.idp.getX509Certificate('signing'),
209
- isBase64Output: false,
210
- };
211
- // step: sign assertion ? -> encrypted ? -> sign message ?
212
- if (metadata.sp.isWantAssertionsSigned()) {
213
- rawSamlResponse = libsaml.constructSAMLSignature({
214
- ...config,
215
- rawSamlMessage: rawSamlResponse,
216
- transformationAlgorithms: spSetting.transformationAlgorithms,
217
- referenceTagXPath: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']",
218
- signatureConfig: {
219
- prefix: 'ds',
220
- location: {
221
- reference: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']/*[local-name(.)='Issuer']",
222
- action: 'after'
223
- },
224
- },
225
- });
226
- }
227
- // console.debug('after assertion signed', rawSamlResponse);
228
- // SAML response must be signed sign message first, then encrypt
229
- if (!encryptThenSign && (spSetting.wantMessageSigned || !metadata.sp.isWantAssertionsSigned())) {
230
- // console.debug('sign then encrypt and sign entire message');
231
- // @ts-ignore
232
- rawSamlResponse = libsaml.constructSAMLSignature({
233
- ...config,
234
- rawSamlMessage: rawSamlResponse,
235
- isMessageSigned: true,
236
- transformationAlgorithms: spSetting.transformationAlgorithms,
237
- signatureConfig: spSetting.signatureConfig || {
238
- prefix: 'ds',
239
- location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Issuer']", action: 'after' },
240
- },
241
- });
242
- }
243
- // console.debug('after message signed', rawSamlResponse);
244
- if (idpSetting.isAssertionEncrypted) {
245
- // console.debug('idp is configured to do encryption');
246
- const context = await libsaml.encryptAssertion(entity.idp, entity.sp, rawSamlResponse);
247
- if (encryptThenSign) {
248
- //need to decode it
249
- rawSamlResponse = utility.base64Decode(context);
250
- }
251
- else {
252
- return Promise.resolve({ id, context });
253
- }
254
- }
255
- //sign after encrypting
256
- if (encryptThenSign && (spSetting.wantMessageSigned || !metadata.sp.isWantAssertionsSigned())) {
257
- // @ts-ignore
258
- rawSamlResponse = libsaml.constructSAMLSignature({
259
- ...config,
260
- rawSamlMessage: rawSamlResponse,
261
- isMessageSigned: true,
262
- transformationAlgorithms: spSetting.transformationAlgorithms,
263
- signatureConfig: spSetting.signatureConfig || {
264
- prefix: 'ds',
265
- location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Issuer']", action: 'after' },
266
- },
267
- });
268
- }
269
- return Promise.resolve({
270
- id,
271
- context: utility.base64Encode(rawSamlResponse),
272
- });
171
+ if (!metadata.idp || !metadata.sp) {
172
+ throw new Error('ERR_GENERATE_ARTIFACT_MISSING_METADATA');
273
173
  }
274
- throw new Error('ERR_GENERATE_POST_LOGIN_RESPONSE_MISSING_METADATA');
174
+ // Generate the base SAML Response using POST binding logic
175
+ const samlResponseResult = await postBinding.base64LoginResponse(params);
176
+ const samlResponseXml = utility.base64Decode(samlResponseResult.context);
177
+ // Generate the SAML 2.0 Artifact ID
178
+ const artifactId = generateArtifactId(metadata.idp.getEntityID());
179
+ // Prepare config for SOAP signing
180
+ const idpSetting = entity.idp.entitySetting;
181
+ const spSetting = entity.sp.entitySetting;
182
+ const config = {
183
+ privateKey: idpSetting.privateKey,
184
+ privateKeyPass: idpSetting.privateKeyPass,
185
+ signatureAlgorithm: idpSetting.requestSignatureAlgorithm,
186
+ signingCert: metadata.idp.getX509Certificate('signing'),
187
+ isBase64Output: false,
188
+ };
189
+ // Construct the SOAP Envelope containing the ArtifactResponse with SAML Response
190
+ const soapBodyContent = `<samlp:ArtifactResponse
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>
198
+ <samlp:Status>
199
+ <samlp:StatusCode Value="${StatusCode.Success}"/>
200
+ </samlp:Status>
201
+ ${samlResponseXml}
202
+ </samlp:ArtifactResponse>`;
203
+ const soapEnvelope = `
204
+ <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
205
+ <soap:Header/>
206
+ <soap:Body>${soapBodyContent}</soap:Body>
207
+ </soap:Envelope>`;
208
+ // Sign the SOAP Envelope
209
+ const signedSoapEnvelope = libsaml.constructSAMLSignature({
210
+ ...config,
211
+ rawSamlMessage: soapEnvelope,
212
+ transformationAlgorithms: spSetting.transformationAlgorithms,
213
+ isMessageSigned: true,
214
+ referenceTagXPath: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']",
215
+ signatureConfig: {
216
+ prefix: 'ds',
217
+ location: {
218
+ reference: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']/*[local-name(.)='Status']",
219
+ action: 'before'
220
+ },
221
+ },
222
+ });
223
+ // Return the Artifact ID and the Base64 encoded signed SOAP message
224
+ return {
225
+ id: artifactId,
226
+ context: utility.base64Encode(signedSoapEnvelope),
227
+ };
275
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
+ */
276
237
  async function parseLoginRequestResolve(params) {
277
- let { idp, sp, xml, } = params;
238
+ const { idp, sp, xml } = params;
278
239
  const verificationOptions = {
279
240
  metadata: idp.entityMeta,
280
241
  signatureAlgorithm: idp.entitySetting.requestSignatureAlgorithm,
281
242
  };
282
- let res = await libsaml.isValidXml(xml, true).catch((error) => {
243
+ // Validate XML
244
+ let res = await libsaml.isValidXml(xml, true).catch(() => {
283
245
  return Promise.reject('ERR_EXCEPTION_VALIDATE_XML');
284
246
  });
285
247
  if (res !== true) {
286
248
  return Promise.reject('ERR_EXCEPTION_VALIDATE_XML');
287
249
  }
288
- /** 首先先验证签名*/
289
- // @ts-ignore
290
- 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);
291
252
  if (!verify) {
292
253
  return Promise.reject('ERR_FAIL_TO_VERIFY_SIGNATURE');
293
254
  }
@@ -295,25 +256,32 @@ async function parseLoginRequestResolve(params) {
295
256
  samlContent: xmlString,
296
257
  extract: extract(xmlString, artifactResolveFields),
297
258
  };
298
- /**
299
- * Validation part: validate the context of response after signature is verified and decrypted (optional)
300
- */
259
+ // Validation
301
260
  const targetEntityMetadata = sp.entityMeta;
302
261
  const issuer = targetEntityMetadata.getEntityID();
303
262
  const extractedProperties = parseResult.extract;
304
- // unmatched issuer
263
+ // Check issuer
305
264
  if (extractedProperties.issuer !== issuer) {
306
265
  return Promise.reject('ERR_UNMATCH_ISSUER');
307
266
  }
308
- // invalid session time
309
- // only run the verifyTime when `SessionNotOnOrAfter` exists
310
- if (!verifyTime(undefined, new Date(new Date(extractedProperties.request.issueInstant).getTime() + 5 * 60 * 1000).toISOString(), sp.entitySetting.clockDrifts)) {
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)) {
311
271
  return Promise.reject('ERR_EXPIRED_SESSION');
312
272
  }
313
273
  return Promise.resolve(parseResult);
314
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
+ */
315
283
  async function parseLoginResponseResolve(params) {
316
- let { idp, sp, art } = params;
284
+ const { idp, sp, art } = params;
317
285
  const metadata = {
318
286
  idp: idp.entityMeta,
319
287
  sp: sp.entityMeta,
@@ -322,14 +290,11 @@ async function parseLoginResponseResolve(params) {
322
290
  metadata: idp.entityMeta,
323
291
  signatureAlgorithm: idp.entitySetting.requestSignatureAlgorithm,
324
292
  };
325
- let parserType = 'SAMLResponse';
326
- /** 断言是否加密应根据响应里面的字段判断*/
327
- let decryptRequired = idp.entitySetting.isAssertionEncrypted;
328
- let extractorFields = [];
329
- let samlContent = '';
293
+ const parserType = ParserType.SAMLResponse;
330
294
  const spSetting = sp.entitySetting;
331
- let ID = '_' + randomUUID();
332
- let url = metadata.idp.getArtifactResolutionService('soap');
295
+ const ID = '_' + randomUUID();
296
+ const url = metadata.idp.getArtifactResolutionService('soap');
297
+ // Construct ArtifactResolve request
333
298
  let samlSoapRaw = libsaml.replaceTagsByValue(libsaml.defaultArtifactResolveTemplate.context, {
334
299
  ID: ID,
335
300
  Destination: url,
@@ -337,31 +302,36 @@ async function parseLoginResponseResolve(params) {
337
302
  IssueInstant: new Date().toISOString(),
338
303
  Art: art
339
304
  });
305
+ let samlContent;
306
+ // Send signed or unsigned ArtifactResolve based on IdP configuration
340
307
  if (!metadata.idp.isWantAuthnRequestsSigned()) {
341
308
  samlContent = await sendArtifactResolve(url, samlSoapRaw);
342
- // check status based on different scenarios
343
- // validate the xml
309
+ // Validate XML
344
310
  try {
345
311
  await libsaml.isValidXml(samlContent, true);
346
312
  }
347
- catch (e) {
313
+ catch {
348
314
  return Promise.reject('ERR_INVALID_XML');
349
315
  }
350
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;
351
323
  }
352
- if (metadata.idp.isWantAuthnRequestsSigned()) {
324
+ else {
353
325
  const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
354
- //@ts-ignore
355
- let signatureSoap = libsaml.constructSAMLSignature({
326
+ // Sign the ArtifactResolve request
327
+ const signatureSoap = libsaml.constructSAMLSignature({
356
328
  referenceTagXPath: "//*[local-name(.)='ArtifactResolve']",
357
329
  isMessageSigned: false,
358
330
  isBase64Output: false,
359
331
  transformationAlgorithms: transformationAlgorithms,
360
- //@ts-ignore
361
- privateKey,
332
+ privateKey: privateKey,
362
333
  privateKeyPass,
363
- //@ts-ignore
364
- signatureAlgorithm,
334
+ signatureAlgorithm: signatureAlgorithm,
365
335
  rawSamlMessage: samlSoapRaw,
366
336
  signingCert: metadata.sp.getX509Certificate('signing'),
367
337
  signatureConfig: {
@@ -373,26 +343,23 @@ async function parseLoginResponseResolve(params) {
373
343
  }
374
344
  });
375
345
  samlContent = await sendArtifactResolve(url, signatureSoap);
376
- // check status based on different scenarios
377
- // validate the xml
346
+ // Validate XML
378
347
  try {
379
348
  await libsaml.isValidXml(samlContent, true);
380
349
  }
381
- catch (e) {
350
+ catch {
382
351
  return Promise.reject('ERR_INVALID_XML');
383
352
  }
384
353
  await checkStatus(samlContent, parserType, true);
385
- const [verified1, verifiedAssertionNode1, isDecryptRequired1, noSignature1] = await libsamlSoap.verifyAndDecryptSoapMessage(samlContent, verificationOptions);
386
- /* decryptRequired = isDecryptRequired*/
354
+ // Verify and decrypt SOAP response
355
+ const [verified1, verifiedAssertionNode1, isDecryptRequired1] = await libsamlSoap.verifyAndDecryptSoapMessage(samlContent, verificationOptions);
387
356
  if (!verified1) {
388
357
  return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
389
358
  }
390
359
  samlContent = verifiedAssertionNode1;
391
- // 改进的postFlow函数中关于签名验证的部分
392
- const verificationResult = await libsaml.verifySignature(samlContent, verificationOptions, self);
393
- // 检查验证结果
360
+ // Verify SAML signature and decrypt if needed
361
+ const verificationResult = await libsaml.verifySignature(samlContent, verificationOptions, sp);
394
362
  if (!verificationResult.status) {
395
- // 如果验证失败,根据具体情况返回错误
396
363
  if (verificationResult.isMessageSigned && !verificationResult.MessageSignatureStatus) {
397
364
  return Promise.reject('ERR_FAIL_TO_VERIFY_MESSAGE_SIGNATURE');
398
365
  }
@@ -402,109 +369,51 @@ async function parseLoginResponseResolve(params) {
402
369
  if (verificationResult.encrypted && !verificationResult.decrypted) {
403
370
  return Promise.reject('ERR_FAIL_TO_DECRYPT_ASSERTION');
404
371
  }
405
- // 通用验证失败
406
372
  return Promise.reject('ERR_FAIL_TO_VERIFY_SIGNATURE_OR_DECRYPTION');
407
373
  }
408
- // 更新samlContent为验证后的版本(可能已解密)
409
374
  samlContent = verificationResult.samlContent;
410
- // 根据验证结果设置extractorFields
411
- if (verificationResult.assertionContent) {
412
- extractorFields = getDefaultExtractorFields(parserType, verificationResult.assertionContent);
413
- }
414
- else {
415
- // 如果没有断言内容(例如注销请求/响应),使用适当的处理方式
416
- extractorFields = getDefaultExtractorFields(parserType, null);
417
- }
418
- const parseResult = {
419
- samlContent: samlContent,
420
- extract: extract(samlContent, extractorFields),
421
- };
422
- /**
423
- * Validation part: validate the context of response after signature is verified and decrypted (optional)
424
- */
425
- const targetEntityMetadata = idp.entityMeta;
426
- const issuer = targetEntityMetadata.getEntityID();
427
- const extractedProperties = parseResult.extract;
428
- // unmatched issuer
429
- if ((parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
430
- && extractedProperties
431
- && extractedProperties.issuer !== issuer) {
432
- return Promise.reject('ERR_UNMATCH_ISSUER');
433
- }
434
- // invalid session time
435
- // only run the verifyTime when `SessionNotOnOrAfter` exists
436
- if (parserType === 'SAMLResponse'
437
- && extractedProperties.sessionIndex.sessionNotOnOrAfter
438
- && !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, sp.entitySetting.clockDrifts)) {
439
- return Promise.reject('ERR_EXPIRED_SESSION');
440
- }
441
- // invalid time
442
- // 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
443
- if (parserType === 'SAMLResponse'
444
- && extractedProperties.conditions
445
- && !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, sp.entitySetting.clockDrifts)) {
446
- return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
447
- }
448
- 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);
449
383
  }
450
384
  const parseResult = {
451
385
  samlContent: samlContent,
452
386
  extract: extract(samlContent, extractorFields),
453
387
  };
454
- /**
455
- * Validation part: validate the context of response after signature is verified and decrypted (optional)
456
- */
388
+ // Validation
457
389
  const targetEntityMetadata = idp.entityMeta;
458
390
  const issuer = targetEntityMetadata.getEntityID();
459
391
  const extractedProperties = parseResult.extract;
460
- // unmatched issuer
461
- if ((parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
392
+ // Check issuer
393
+ if (parserType === ParserType.SAMLResponse
462
394
  && extractedProperties
463
395
  && extractedProperties.issuer !== issuer) {
464
396
  return Promise.reject('ERR_UNMATCH_ISSUER');
465
397
  }
466
- // invalid session time
467
- // only run the verifyTime when `SessionNotOnOrAfter` exists
398
+ // Check session time
468
399
  if (parserType === 'SAMLResponse'
469
400
  && extractedProperties.sessionIndex.sessionNotOnOrAfter
470
401
  && !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, sp.entitySetting.clockDrifts)) {
471
402
  return Promise.reject('ERR_EXPIRED_SESSION');
472
403
  }
473
- // invalid time
474
- // 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
404
+ // Check conditions time
475
405
  if (parserType === 'SAMLResponse'
476
406
  && extractedProperties.conditions
477
407
  && !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, sp.entitySetting.clockDrifts)) {
478
408
  return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
479
409
  }
480
- //valid destination
481
- //There is no validation of the response here. The upper-layer application
482
- // should verify the result by itself to see if the destination is equal to the SP acs and
483
- // whether the response.id is used to prevent replay attacks.
484
- /*
485
- let destination = extractedProperties?.response?.destination
486
- let isExit = self.entitySetting?.assertionConsumerService?.filter((item) => {
487
- return item?.Location === destination
488
- })
489
- if (isExit?.length === 0) {
490
- return Promise.reject('ERR_Destination_URL');
491
- }
492
- if (parserType === 'SAMLResponse') {
493
- let destination = extractedProperties?.response?.destination
494
- let isExit = self.entitySetting?.assertionConsumerService?.filter((item: { Location: any; }) => {
495
- return item?.Location === destination
496
- })
497
- if (isExit?.length === 0) {
498
- return Promise.reject('ERR_Destination_URL');
499
- }
500
- }
501
- */
502
410
  return Promise.resolve(parseResult);
503
411
  }
504
- const artifactSignBinding = {
505
- parseLoginRequestResolve,
412
+ const artifactBinding = {
506
413
  soapLoginRequest,
507
- parseLoginResponseResolve,
508
414
  soapLoginResponse,
415
+ parseLoginRequestResolve,
416
+ parseLoginResponseResolve,
417
+ generateArtifactId,
509
418
  };
510
- export default artifactSignBinding;
419
+ export default artifactBinding;