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.
@@ -1,424 +1,538 @@
1
1
  /**
2
2
  * @file binding-artifact.ts
3
- * @author tngan
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
3
+ * @desc Binding-level API for SAML 2.0 HTTP-Artifact Binding
4
+ *
5
+ * The Artifact binding has two distinct layers:
6
+ * 1. Front-channel delivery of `SAMLart`
7
+ * 2. Back-channel SOAP `ArtifactResolve` / `ArtifactResponse`
6
8
  */
7
- import { checkStatus } from "./flow.js";
8
- import { ParserType, StatusCode, wording } from './urn.js';
9
- import * as crypto from "node:crypto";
9
+ import { ParserType, StatusCode, namespace, wording } from './urn.js';
10
10
  import libsaml from './libsaml.js';
11
11
  import libsamlSoap from './libsamlSoap.js';
12
12
  import utility, { get } from './utility.js';
13
- import { randomUUID } from 'node:crypto';
14
13
  import postBinding from './binding-post.js';
15
- import { artifactResolveFields, extract, loginRequestFields, loginResponseFields, logoutRequestFields, logoutResponseFields } from "./extractor.js";
16
- import { verifyTime } from "./validator.js";
17
- import { sendArtifactResolve } from "./soap.js";
14
+ import { artifactResolveFields, artifactResponseFields, extract, loginRequestFields, } from './extractor.js';
15
+ import { verifyTime } from './validator.js';
16
+ import { sendArtifactResolve } from './soap.js';
17
+ import { generateArtifactId as generateArtifactIdUtil, validateArtifact, } from './artifact.js';
18
18
  import { applyAuthnRequestEnhancements } from './saml2-enhancements-integration.js';
19
+ import { flow } from './flow.js';
19
20
  const binding = wording.binding;
20
- /**
21
- * Get default extractor fields based on parser type
22
- */
23
- function getDefaultExtractorFields(parserType, assertion) {
24
- switch (parserType) {
25
- case ParserType.SAMLRequest:
26
- return loginRequestFields;
27
- case ParserType.SAMLResponse:
28
- if (!assertion) {
29
- throw new Error('ERR_EMPTY_ASSERTION');
21
+ const SAML_ARTIFACT_PARAM = 'SAMLart';
22
+ function fail(code) {
23
+ throw code;
24
+ }
25
+ function normalizeBoolean(value) {
26
+ if (typeof value === 'boolean') {
27
+ return value;
28
+ }
29
+ if (typeof value === 'string') {
30
+ const lowered = value.trim().toLowerCase();
31
+ if (lowered === 'true') {
32
+ return true;
33
+ }
34
+ if (lowered === 'false') {
35
+ return false;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ function normalizeIndex(value, fallback) {
41
+ const parsed = Number.parseInt(String(value ?? fallback), 10);
42
+ return Number.isNaN(parsed) ? fallback : parsed;
43
+ }
44
+ function getServiceCandidates(entityMeta, key) {
45
+ const rawValue = entityMeta?.meta?.[key];
46
+ if (!rawValue) {
47
+ return [];
48
+ }
49
+ const normalizeRecord = (item, order, bindingKey) => {
50
+ if (item == null) {
51
+ return null;
52
+ }
53
+ if (typeof item === 'string') {
54
+ return {
55
+ binding: String(bindingKey || ''),
56
+ location: item,
57
+ index: order,
58
+ isDefault: order === 0 ? true : null,
59
+ order,
60
+ };
61
+ }
62
+ const location = String(item.location ??
63
+ item.Location ??
64
+ item.responseLocation ??
65
+ item.ResponseLocation ??
66
+ '').trim();
67
+ if (!location) {
68
+ return null;
69
+ }
70
+ return {
71
+ binding: String(item.binding ?? item.Binding ?? bindingKey ?? '').trim(),
72
+ location,
73
+ index: normalizeIndex(item.index ?? item.Index, order),
74
+ isDefault: normalizeBoolean(item.isDefault ?? item.IsDefault ?? item.default),
75
+ order,
76
+ };
77
+ };
78
+ if (Array.isArray(rawValue)) {
79
+ return rawValue
80
+ .map((item, index) => normalizeRecord(item, index))
81
+ .filter((item) => item !== null);
82
+ }
83
+ if (typeof rawValue === 'object') {
84
+ const looksLikeSingleRecord = ('location' in rawValue ||
85
+ 'Location' in rawValue ||
86
+ 'binding' in rawValue ||
87
+ 'Binding' in rawValue);
88
+ if (looksLikeSingleRecord) {
89
+ const record = normalizeRecord(rawValue, 0);
90
+ return record ? [record] : [];
91
+ }
92
+ return Object.entries(rawValue)
93
+ .map(([candidateBinding, candidateLocation], index) => normalizeRecord(candidateLocation, index, candidateBinding))
94
+ .filter((item) => item !== null);
95
+ }
96
+ return [];
97
+ }
98
+ function resolveBindingUri(bindingName) {
99
+ if (!bindingName) {
100
+ return null;
101
+ }
102
+ const mapped = namespace.binding[bindingName];
103
+ if (typeof mapped === 'string') {
104
+ return mapped;
105
+ }
106
+ return bindingName;
107
+ }
108
+ function pickPreferredService(entityMeta, key, bindingName) {
109
+ const candidates = getServiceCandidates(entityMeta, key);
110
+ if (candidates.length === 0) {
111
+ return null;
112
+ }
113
+ const expectedBinding = resolveBindingUri(bindingName);
114
+ const filtered = expectedBinding
115
+ ? candidates.filter((item) => item.binding === expectedBinding)
116
+ : candidates;
117
+ const pool = filtered.length > 0 ? filtered : candidates;
118
+ return [...pool].sort((left, right) => {
119
+ const leftScore = left.isDefault === true ? 0 : (left.isDefault === null ? 1 : 2);
120
+ const rightScore = right.isDefault === true ? 0 : (right.isDefault === null ? 1 : 2);
121
+ if (leftScore !== rightScore) {
122
+ return leftScore - rightScore;
123
+ }
124
+ if (left.index !== right.index) {
125
+ return left.index - right.index;
126
+ }
127
+ return left.order - right.order;
128
+ })[0] ?? null;
129
+ }
130
+ function ensureServiceLocation(entityMeta, key, bindingName, errorCode) {
131
+ const expectedBinding = resolveBindingUri(bindingName);
132
+ const candidates = getServiceCandidates(entityMeta, key);
133
+ const filtered = expectedBinding
134
+ ? candidates.filter((item) => item.binding === expectedBinding)
135
+ : candidates;
136
+ const candidate = filtered.length > 0
137
+ ? [...filtered].sort((left, right) => {
138
+ const leftScore = left.isDefault === true ? 0 : (left.isDefault === null ? 1 : 2);
139
+ const rightScore = right.isDefault === true ? 0 : (right.isDefault === null ? 1 : 2);
140
+ if (leftScore !== rightScore) {
141
+ return leftScore - rightScore;
142
+ }
143
+ if (left.index !== right.index) {
144
+ return left.index - right.index;
30
145
  }
31
- return loginResponseFields(assertion);
32
- case ParserType.LogoutRequest:
33
- return logoutRequestFields;
34
- case ParserType.LogoutResponse:
35
- return logoutResponseFields;
36
- default:
37
- throw new Error('ERR_UNDEFINED_PARSERTYPE');
146
+ return left.order - right.order;
147
+ })[0]
148
+ : null;
149
+ if (!candidate?.location) {
150
+ fail(errorCode);
38
151
  }
152
+ return candidate.location;
39
153
  }
40
- /**
41
- * Generate a SAML 2.0 compliant Artifact ID
42
- * Format: [TypeCode: 2 bytes] + [EndpointIndex: 2 bytes] + [SourceID: 20 bytes] + [MessageHandle: 20 bytes]
43
- * @param issuerId The entity ID of the issuing party (IdP)
44
- * @param endpointIndex The index of the destination endpoint (default is 1 for Artifact Resolution Service)
45
- * @returns The Base64 encoded Artifact ID string
46
- */
47
- export function generateArtifactId(issuerId, endpointIndex = 1) {
48
- // SourceID: 20 bytes SHA-1 of issuer ID
49
- const issuerHash = crypto.createHash('sha1').update(issuerId).digest();
50
- const sourceId = issuerHash.subarray(0, 20);
51
- // TypeCode: 0x0004 (SAML 2.0 Artifact Type)
52
- const typeCode = Buffer.from([0x00, 0x04]);
53
- // EndpointIndex: 2 bytes Big Endian
54
- const indexBuffer = Buffer.alloc(2);
55
- indexBuffer.writeUInt16BE(endpointIndex, 0);
56
- // MessageHandle: 20 random bytes
57
- const messageHandle = crypto.randomBytes(20);
58
- // Concatenate: 2 + 2 + 20 + 20 = 44 bytes
59
- const artifactBytes = Buffer.concat([typeCode, indexBuffer, sourceId, messageHandle]);
60
- // Base64 encode
61
- return artifactBytes.toString('base64');
154
+ function ensureValidDestination(entityMeta, key, destination, bindingName, errorCode) {
155
+ if (!destination) {
156
+ return;
157
+ }
158
+ const expectedBinding = resolveBindingUri(bindingName);
159
+ const candidates = getServiceCandidates(entityMeta, key);
160
+ const matched = candidates.some((item) => {
161
+ if (expectedBinding && item.binding && item.binding !== expectedBinding) {
162
+ return false;
163
+ }
164
+ return item.location === destination;
165
+ });
166
+ if (!matched) {
167
+ fail(errorCode);
168
+ }
62
169
  }
63
- /**
64
- * @desc Generate a SOAP-encoded login request for Artifact binding
65
- * @param {string} referenceTagXPath reference uri
66
- * @param {object} entity object includes both idp and sp
67
- * @param {function} customTagReplacement used when developers have their own login request template
68
- * @returns {BindingContext}
69
- */
70
- function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
170
+ function getArtifactEndpointIndex(entityMeta) {
171
+ return pickPreferredService(entityMeta, 'artifactResolutionService', binding.soap)?.index ?? 0;
172
+ }
173
+ function getArtifactFromRequest(request) {
174
+ const artifact = request?.query?.[SAML_ARTIFACT_PARAM] ?? request?.body?.[SAML_ARTIFACT_PARAM];
175
+ if (typeof artifact !== 'string' || artifact.trim() === '') {
176
+ fail('ERR_MISSING_ARTIFACT');
177
+ }
178
+ const relayState = request?.query?.RelayState ?? request?.body?.RelayState ?? '';
179
+ return {
180
+ artifact,
181
+ relayState,
182
+ };
183
+ }
184
+ function ensureArtifactResolveIssueInstant(issueInstant, clockDrifts) {
185
+ if (!issueInstant) {
186
+ fail('ERR_INVALID_ISSUE_INSTANT');
187
+ }
188
+ const issuedAt = new Date(issueInstant);
189
+ if (Number.isNaN(issuedAt.getTime())) {
190
+ fail('ERR_INVALID_ISSUE_INSTANT');
191
+ }
192
+ const expiry = new Date(issuedAt.getTime() + 5 * 60 * 1000).toISOString();
193
+ if (!verifyTime(undefined, expiry, clockDrifts)) {
194
+ fail('ERR_EXPIRED_SESSION');
195
+ }
196
+ }
197
+ function buildRawLoginRequest(referenceTagXPath, entity, customTagReplacement) {
71
198
  const metadata = {
72
199
  idp: entity.idp.entityMeta,
73
200
  sp: entity.sp.entityMeta,
74
- inResponse: entity.inResponse,
75
- relayState: entity.relayState
76
201
  };
77
202
  const spSetting = entity.sp.entitySetting;
78
203
  let id = '';
79
- let soapId = spSetting.generateID();
80
- if (metadata && metadata.idp && metadata.sp) {
81
- const base = metadata.idp.getSingleSignOnService(binding.post);
82
- let rawSamlRequest;
83
- if (spSetting.loginRequestTemplate && customTagReplacement) {
84
- const info = customTagReplacement(spSetting.loginRequestTemplate.context);
85
- id = get(info, 'id', null);
86
- rawSamlRequest = get(info, 'context', null);
87
- }
88
- else {
89
- const nameIDFormat = spSetting.nameIDFormat;
90
- const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat;
91
- id = spSetting.generateID();
92
- rawSamlRequest = libsaml.replaceTagsByValue(libsaml.defaultLoginRequestTemplate.context, {
93
- ID: id,
94
- Destination: base,
95
- Issuer: metadata.sp.getEntityID(),
96
- IssueInstant: new Date().toISOString(),
97
- AssertionConsumerServiceURL: metadata.sp.getAssertionConsumerService(binding.post),
98
- EntityID: metadata.sp.getEntityID(),
99
- AllowCreate: spSetting.allowCreate,
100
- NameIDFormat: selectedNameIDFormat
101
- });
102
- }
103
- // 应用 AuthnRequest 增强功能
104
- if (spSetting.authnRequestEnhancements) {
105
- rawSamlRequest = applyAuthnRequestEnhancements(rawSamlRequest, spSetting.authnRequestEnhancements);
106
- }
107
- const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
108
- let signedAuthnRequest;
109
- if (metadata.idp.isWantAuthnRequestsSigned()) {
110
- signedAuthnRequest = libsaml.constructSAMLSignature({
111
- referenceTagXPath,
112
- privateKey: privateKey,
113
- privateKeyPass,
114
- signatureAlgorithm: signatureAlgorithm,
115
- transformationAlgorithms,
116
- rawSamlMessage: rawSamlRequest,
117
- isBase64Output: false,
118
- signingCert: metadata.sp.getX509Certificate('signing'),
119
- signatureConfig: spSetting.signatureConfig || {
120
- prefix: 'ds',
121
- location: {
122
- reference: "/*[local-name(.)='AuthnRequest']/*[local-name(.)='Issuer']",
123
- action: 'after'
124
- },
125
- }
126
- });
127
- }
128
- else {
129
- signedAuthnRequest = rawSamlRequest;
130
- }
131
- // Construct SOAP envelope with ArtifactResponse containing AuthnRequest
132
- const soapTemplate = libsaml.replaceTagsByValue(libsaml.defaultArtAuthnRequestTemplate.context, {
133
- ID: soapId,
134
- IssueInstant: new Date().toISOString(),
135
- InResponseTo: metadata.inResponse ?? "",
204
+ if (!metadata.idp || !metadata.sp) {
205
+ fail('ERR_GENERATE_ARTIFACT_LOGIN_REQUEST_MISSING_METADATA');
206
+ }
207
+ const destination = ensureServiceLocation(metadata.idp, 'singleSignOnService', binding.artifact, 'ERR_MISSING_IDP_ARTIFACT_ENDPOINT');
208
+ const artifactAcs = pickPreferredService(metadata.sp, 'assertionConsumerService', binding.artifact)?.location;
209
+ if (!artifactAcs) {
210
+ fail('ERR_MISSING_SP_ARTIFACT_ACS');
211
+ }
212
+ let rawSamlRequest;
213
+ if (spSetting.loginRequestTemplate && customTagReplacement) {
214
+ const info = customTagReplacement(spSetting.loginRequestTemplate.context);
215
+ id = get(info, 'id', '');
216
+ rawSamlRequest = get(info, 'context', '');
217
+ }
218
+ else {
219
+ const nameIDFormat = spSetting.nameIDFormat;
220
+ const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat;
221
+ id = spSetting.generateID();
222
+ rawSamlRequest = libsaml.replaceTagsByValue(libsaml.defaultLoginRequestTemplate.context, {
223
+ ID: id,
224
+ Destination: destination,
136
225
  Issuer: metadata.sp.getEntityID(),
137
- AuthnRequest: signedAuthnRequest
226
+ IssueInstant: new Date().toISOString(),
227
+ AssertionConsumerServiceURL: artifactAcs,
228
+ EntityID: metadata.sp.getEntityID(),
229
+ AllowCreate: spSetting.allowCreate,
230
+ NameIDFormat: selectedNameIDFormat,
138
231
  });
139
- // Sign the SOAP envelope
140
- const signedSoap = libsaml.constructSAMLSignature({
141
- referenceTagXPath: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']",
142
- privateKey: privateKey,
143
- privateKeyPass,
144
- signatureAlgorithm: signatureAlgorithm,
145
- transformationAlgorithms,
146
- rawSamlMessage: soapTemplate,
232
+ rawSamlRequest = rawSamlRequest.replace(`ProtocolBinding="${namespace.binding.post}"`, `ProtocolBinding="${namespace.binding.artifact}"`);
233
+ }
234
+ if (spSetting.authnRequestEnhancements) {
235
+ rawSamlRequest = applyAuthnRequestEnhancements(rawSamlRequest, spSetting.authnRequestEnhancements);
236
+ }
237
+ if (!metadata.idp.isWantAuthnRequestsSigned()) {
238
+ return {
239
+ id,
240
+ context: rawSamlRequest,
241
+ };
242
+ }
243
+ return {
244
+ id,
245
+ context: libsaml.constructSAMLSignature({
246
+ referenceTagXPath,
247
+ privateKey: spSetting.privateKey,
248
+ privateKeyPass: spSetting.privateKeyPass,
249
+ signatureAlgorithm: spSetting.requestSignatureAlgorithm,
250
+ transformationAlgorithms: spSetting.transformationAlgorithms,
251
+ rawSamlMessage: rawSamlRequest,
147
252
  isBase64Output: false,
148
- isMessageSigned: false,
149
253
  signingCert: metadata.sp.getX509Certificate('signing'),
150
- signatureConfig: {
254
+ signatureConfig: (spSetting.signatureConfig || {
151
255
  prefix: 'ds',
152
256
  location: {
153
- reference: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']/*[local-name(.)='Issuer']",
154
- action: 'after'
155
- }
156
- },
157
- });
158
- return {
159
- id: soapId,
160
- context: signedSoap,
161
- };
162
- }
163
- throw new Error('ERR_GENERATE_POST_LOGIN_REQUEST_MISSING_METADATA');
257
+ reference: "/*[local-name(.)='AuthnRequest']/*[local-name(.)='Issuer']",
258
+ action: 'after',
259
+ },
260
+ }),
261
+ }),
262
+ };
164
263
  }
165
- /**
166
- * @desc Generate a SOAP-encoded login response for Artifact binding
167
- * @param {Base64LoginResponseParams} params parameters for generating login response
168
- * @returns {BindingContext}
169
- */
170
- async function soapLoginResponse(params) {
171
- const { entity } = params;
172
- const metadata = {
173
- idp: entity.idp.entityMeta,
174
- sp: entity.sp.entityMeta,
264
+ async function buildRawLoginResponse(params) {
265
+ if (!pickPreferredService(params.entity.sp.entityMeta, 'assertionConsumerService', binding.artifact)) {
266
+ fail('ERR_MISSING_SP_ARTIFACT_ACS');
267
+ }
268
+ const result = await postBinding.base64LoginResponse({
269
+ ...params,
270
+ destinationBinding: binding.artifact,
271
+ });
272
+ return {
273
+ id: result.id,
274
+ context: utility.base64Decode(result.context),
175
275
  };
176
- if (!metadata.idp || !metadata.sp) {
177
- throw new Error('ERR_GENERATE_ARTIFACT_MISSING_METADATA');
178
- }
179
- // Generate the base SAML Response using POST binding logic
180
- const samlResponseResult = await postBinding.base64LoginResponse(params);
181
- const samlResponseXml = utility.base64Decode(samlResponseResult.context);
182
- // Generate the SAML 2.0 Artifact ID
183
- const artifactId = generateArtifactId(metadata.idp.getEntityID());
184
- // Prepare config for SOAP signing
185
- const idpSetting = entity.idp.entitySetting;
186
- const spSetting = entity.sp.entitySetting;
187
- const config = {
188
- privateKey: idpSetting.privateKey,
189
- privateKeyPass: idpSetting.privateKeyPass,
190
- signatureAlgorithm: idpSetting.requestSignatureAlgorithm,
191
- signingCert: metadata.idp.getX509Certificate('signing'),
276
+ }
277
+ function signSoapEnvelope(message, referenceTagXPath, signatureReference, signer) {
278
+ const signerSetting = signer.entitySetting;
279
+ const signingCert = signer.entityMeta.getX509Certificate('signing');
280
+ if (!signerSetting.privateKey || !signingCert) {
281
+ fail('ERR_MISSING_ARTIFACT_RESOLVE_CREDENTIALS');
282
+ }
283
+ return libsaml.constructSAMLSignature({
284
+ referenceTagXPath,
285
+ privateKey: signerSetting.privateKey,
286
+ privateKeyPass: signerSetting.privateKeyPass,
287
+ signatureAlgorithm: signerSetting.requestSignatureAlgorithm,
288
+ transformationAlgorithms: signerSetting.transformationAlgorithms,
289
+ rawSamlMessage: message,
192
290
  isBase64Output: false,
193
- };
194
- // Construct the SOAP Envelope containing the ArtifactResponse with SAML Response
195
- const soapBodyContent = `<samlp:ArtifactResponse
196
- xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
197
- xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
198
- ID="${samlResponseResult.id}_artifact_resp"
199
- InResponseTo="${params.requestInfo?.extract?.request?.id || ''}"
200
- Version="2.0"
201
- IssueInstant="${new Date().toISOString()}">
202
- <saml2:Issuer>${metadata.idp.getEntityID()}</saml2:Issuer>
203
- <samlp:Status>
204
- <samlp:StatusCode Value="${StatusCode.Success}"/>
205
- </samlp:Status>
206
- ${samlResponseXml}
207
- </samlp:ArtifactResponse>`;
208
- const soapEnvelope = `
209
- <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
210
- <soap:Header/>
211
- <soap:Body>${soapBodyContent}</soap:Body>
212
- </soap:Envelope>`;
213
- // Sign the SOAP Envelope
214
- const signedSoapEnvelope = libsaml.constructSAMLSignature({
215
- ...config,
216
- rawSamlMessage: soapEnvelope,
217
- transformationAlgorithms: spSetting.transformationAlgorithms,
218
- isMessageSigned: true,
219
- referenceTagXPath: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']",
291
+ isMessageSigned: false,
292
+ signingCert,
220
293
  signatureConfig: {
221
294
  prefix: 'ds',
222
295
  location: {
223
- reference: "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']/*[local-name(.)='Status']",
224
- action: 'before'
296
+ reference: signatureReference,
297
+ action: 'after',
225
298
  },
226
299
  },
227
300
  });
228
- // Return the Artifact ID and the Base64 encoded signed SOAP message
301
+ }
302
+ function createArtifactResolveRequest(params) {
303
+ validateArtifact(params.artifact, params.responder.entityMeta.getEntityID());
304
+ const destination = ensureServiceLocation(params.responder.entityMeta, 'artifactResolutionService', binding.soap, 'ERR_MISSING_ARTIFACT_RESOLUTION_SERVICE');
305
+ const id = params.requester.entitySetting.generateID();
306
+ const soapResolve = libsaml.replaceTagsByValue(libsaml.defaultArtifactResolveTemplate.context, {
307
+ ID: id,
308
+ Destination: destination,
309
+ Issuer: params.requester.entityMeta.getEntityID(),
310
+ IssueInstant: new Date().toISOString(),
311
+ Art: params.artifact,
312
+ });
229
313
  return {
230
- id: artifactId,
231
- context: utility.base64Encode(signedSoapEnvelope),
314
+ id,
315
+ artifact: params.artifact,
316
+ entityEndpoint: destination,
317
+ type: 'ArtifactResolve',
318
+ context: signSoapEnvelope(soapResolve, "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResolve']", "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResolve']/*[local-name(.)='Issuer']", params.requester),
232
319
  };
233
320
  }
234
- /**
235
- * @desc Parse and validate Artifact Resolve request
236
- * @param {object} params
237
- * @param {IdentityProvider} params.idp Identity Provider instance
238
- * @param {ServiceProvider} params.sp Service Provider instance
239
- * @param {string} params.xml SOAP request XML string
240
- * @returns {Promise}
241
- */
242
- async function parseLoginRequestResolve(params) {
243
- const { idp, sp, xml } = params;
244
- const verificationOptions = {
245
- metadata: idp.entityMeta,
246
- signatureAlgorithm: idp.entitySetting.requestSignatureAlgorithm,
321
+ async function parseArtifactResolveRequest(params) {
322
+ const { requester, responder, xml } = params;
323
+ const validXml = await libsaml.isValidXml(xml, true).catch(() => false);
324
+ if (validXml !== true) {
325
+ fail('ERR_EXCEPTION_VALIDATE_XML');
326
+ }
327
+ const verifiedSoap = await libsamlSoap.verifyAndDecryptSoapMessage(xml, {
328
+ metadata: requester.entityMeta,
329
+ });
330
+ if (!verifiedSoap.verified || verifiedSoap.type !== 'ArtifactResolve') {
331
+ fail('ERR_FAIL_TO_VERIFY_SIGNATURE');
332
+ }
333
+ const extracted = extract(xml, artifactResolveFields);
334
+ if (extracted?.request?.version && extracted.request.version !== '2.0') {
335
+ fail('ERR_UNSUPPORTED_SAML_VERSION');
336
+ }
337
+ if (extracted?.issuer !== requester.entityMeta.getEntityID()) {
338
+ fail('ERR_UNMATCH_ISSUER');
339
+ }
340
+ ensureValidDestination(responder.entityMeta, 'artifactResolutionService', extracted?.request?.destination, binding.soap, 'ERR_INVALID_ARTIFACT_RESOLVE_DESTINATION');
341
+ ensureArtifactResolveIssueInstant(extracted?.request?.issueInstant, responder.entitySetting.clockDrifts);
342
+ const artifactData = validateArtifact(extracted?.artifact, responder.entityMeta.getEntityID());
343
+ return {
344
+ soapContent: xml,
345
+ samlContent: verifiedSoap.message,
346
+ extract: extracted,
347
+ artifact: extracted.artifact,
348
+ artifactData,
247
349
  };
248
- // Validate XML
249
- let res = await libsaml.isValidXml(xml, true).catch(() => {
250
- return Promise.reject('ERR_EXCEPTION_VALIDATE_XML');
350
+ }
351
+ function createArtifactResolveResponse(params) {
352
+ const { requester, responder } = params;
353
+ const id = responder.entitySetting.generateID();
354
+ const destination = pickPreferredService(requester.entityMeta, 'artifactResolutionService', binding.soap)?.location || '';
355
+ const statusCode = params.statusCode || StatusCode.Success;
356
+ const template = `<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/><SOAP-ENV:Body><samlp:ArtifactResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" InResponseTo="{InResponseTo}" Version="2.0" IssueInstant="{IssueInstant}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status>{SamlMessage}</samlp:ArtifactResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>`;
357
+ let soapResponse = libsaml.replaceTagsByValue(template, {
358
+ ID: id,
359
+ InResponseTo: params.inResponseTo,
360
+ IssueInstant: new Date().toISOString(),
361
+ Issuer: responder.entityMeta.getEntityID(),
362
+ StatusCode: statusCode,
363
+ SamlMessage: params.samlMessage || '',
251
364
  });
252
- if (res !== true) {
253
- return Promise.reject('ERR_EXCEPTION_VALIDATE_XML');
365
+ if (destination) {
366
+ soapResponse = soapResponse.replace(/(<samlp:ArtifactResponse\b[^>]*IssueInstant="[^"]+")/, `$1 Destination="${destination}"`);
254
367
  }
255
- // Verify and decrypt SOAP message
256
- let [verify, xmlString] = await libsamlSoap.verifyAndDecryptSoapMessage(xml, verificationOptions);
257
- if (!verify) {
258
- return Promise.reject('ERR_FAIL_TO_VERIFY_SIGNATURE');
368
+ return {
369
+ id,
370
+ context: signSoapEnvelope(soapResponse, "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']", "/*[local-name(.)='Envelope']/*[local-name(.)='Body']/*[local-name(.)='ArtifactResponse']/*[local-name(.)='Issuer']", responder),
371
+ };
372
+ }
373
+ async function parseArtifactResolveResponse(params) {
374
+ const validXml = await libsaml.isValidXml(params.xml, true).catch(() => false);
375
+ if (validXml !== true) {
376
+ fail('ERR_INVALID_XML');
259
377
  }
260
- const parseResult = {
261
- samlContent: xmlString,
262
- extract: extract(xmlString, artifactResolveFields),
378
+ const verifiedSoap = await libsamlSoap.verifyAndDecryptSoapMessage(params.xml, {
379
+ metadata: params.responder.entityMeta,
380
+ });
381
+ if (!verifiedSoap.verified || verifiedSoap.type !== 'ArtifactResponse') {
382
+ fail('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
383
+ }
384
+ const extracted = extract(params.xml, artifactResponseFields);
385
+ if (extracted?.response?.version && extracted.response.version !== '2.0') {
386
+ fail('ERR_UNSUPPORTED_SAML_VERSION');
387
+ }
388
+ if (extracted?.issuer !== params.responder.entityMeta.getEntityID()) {
389
+ fail('ERR_UNMATCH_ISSUER');
390
+ }
391
+ if (params.inResponseTo && extracted?.response?.inResponseTo !== params.inResponseTo) {
392
+ fail('ERR_UNMATCH_IN_RESPONSE_TO');
393
+ }
394
+ ensureValidDestination(params.requester.entityMeta, 'artifactResolutionService', extracted?.response?.destination, binding.soap, 'ERR_INVALID_ARTIFACT_RESPONSE_DESTINATION');
395
+ if (extracted?.status !== StatusCode.Success) {
396
+ fail('ERR_UNDEFINED_STATUS');
397
+ }
398
+ if (!verifiedSoap.resolvedMessage) {
399
+ fail('ERR_EMPTY_ARTIFACT_RESPONSE');
400
+ }
401
+ return {
402
+ soapContent: params.xml,
403
+ samlContent: verifiedSoap.resolvedMessage,
404
+ extract: extracted,
263
405
  };
264
- // Validation
265
- const targetEntityMetadata = sp.entityMeta;
266
- const issuer = targetEntityMetadata.getEntityID();
267
- const extractedProperties = parseResult.extract;
268
- // Check issuer
269
- if (extractedProperties.issuer !== issuer) {
270
- return Promise.reject('ERR_UNMATCH_ISSUER');
271
- }
272
- // Check time validity (5 minutes from issue instant)
273
- const issueInstant = new Date(extractedProperties.request.issueInstant);
274
- const expiryTime = new Date(issueInstant.getTime() + 5 * 60 * 1000);
275
- if (!verifyTime(undefined, expiryTime.toISOString(), sp.entitySetting.clockDrifts)) {
276
- return Promise.reject('ERR_EXPIRED_SESSION');
277
- }
278
- return Promise.resolve(parseResult);
279
406
  }
280
- /**
281
- * @desc Parse and validate Artifact Resolve response
282
- * @param {object} params
283
- * @param {IdentityProvider} params.idp Identity Provider instance
284
- * @param {ServiceProvider} params.sp Service Provider instance
285
- * @param {string} params.art Artifact string
286
- * @returns {Promise}
287
- */
288
- async function parseLoginResponseResolve(params) {
289
- const { idp, sp, art } = params;
290
- const metadata = {
291
- idp: idp.entityMeta,
292
- sp: sp.entityMeta,
407
+ function createLoginRequest(referenceTagXPath, entity, customTagReplacement) {
408
+ const request = buildRawLoginRequest(referenceTagXPath, entity, customTagReplacement);
409
+ const artifact = generateArtifactIdUtil(entity.sp.entityMeta.getEntityID(), getArtifactEndpointIndex(entity.sp.entityMeta));
410
+ return {
411
+ id: request.id,
412
+ artifact,
413
+ context: artifact,
414
+ samlContent: request.context,
293
415
  };
294
- const verificationOptions = {
295
- metadata: idp.entityMeta,
296
- signatureAlgorithm: idp.entitySetting.requestSignatureAlgorithm,
416
+ }
417
+ async function createLoginResponse(params) {
418
+ const response = await buildRawLoginResponse(params);
419
+ const artifact = generateArtifactIdUtil(params.entity.idp.entityMeta.getEntityID(), getArtifactEndpointIndex(params.entity.idp.entityMeta));
420
+ return {
421
+ id: response.id,
422
+ artifact,
423
+ context: artifact,
424
+ samlContent: response.context,
297
425
  };
298
- const parserType = ParserType.SAMLResponse;
299
- const spSetting = sp.entitySetting;
300
- const ID = '_' + randomUUID();
301
- const url = metadata.idp.getArtifactResolutionService('soap');
302
- // Construct ArtifactResolve request
303
- let samlSoapRaw = libsaml.replaceTagsByValue(libsaml.defaultArtifactResolveTemplate.context, {
304
- ID: ID,
305
- Destination: url,
306
- Issuer: metadata.sp.getEntityID(),
307
- IssueInstant: new Date().toISOString(),
308
- Art: art
426
+ }
427
+ async function resolveArtifact(params) {
428
+ const resolveRequest = createArtifactResolveRequest(params);
429
+ const responseXml = await sendArtifactResolve(resolveRequest.entityEndpoint, resolveRequest.context);
430
+ const resolved = await parseArtifactResolveResponse({
431
+ requester: params.requester,
432
+ responder: params.responder,
433
+ xml: responseXml,
434
+ inResponseTo: resolveRequest.id,
309
435
  });
310
- let samlContent;
311
- // Send signed or unsigned ArtifactResolve based on IdP configuration
312
- if (!metadata.idp.isWantAuthnRequestsSigned()) {
313
- samlContent = await sendArtifactResolve(url, samlSoapRaw);
314
- // Validate XML
315
- try {
316
- await libsaml.isValidXml(samlContent, true);
317
- }
318
- catch {
319
- return Promise.reject('ERR_INVALID_XML');
320
- }
321
- await checkStatus(samlContent, parserType, true);
322
- // Verify and decrypt SOAP response
323
- const [verified, verifiedAssertionNode, isDecryptRequired] = await libsamlSoap.verifyAndDecryptSoapMessage(samlContent, verificationOptions);
324
- if (!verified) {
325
- return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
326
- }
327
- samlContent = verifiedAssertionNode;
328
- }
329
- else {
330
- const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
331
- // Sign the ArtifactResolve request
332
- const signatureSoap = libsaml.constructSAMLSignature({
333
- referenceTagXPath: "//*[local-name(.)='ArtifactResolve']",
334
- isMessageSigned: false,
335
- isBase64Output: false,
336
- transformationAlgorithms: transformationAlgorithms,
337
- privateKey: privateKey,
338
- privateKeyPass,
339
- signatureAlgorithm: signatureAlgorithm,
340
- rawSamlMessage: samlSoapRaw,
341
- signingCert: metadata.sp.getX509Certificate('signing'),
342
- signatureConfig: {
343
- prefix: 'ds',
344
- location: {
345
- reference: "//*[local-name(.)='Issuer']",
346
- action: 'after'
347
- }
348
- }
349
- });
350
- samlContent = await sendArtifactResolve(url, signatureSoap);
351
- // Validate XML
352
- try {
353
- await libsaml.isValidXml(samlContent, true);
354
- }
355
- catch {
356
- return Promise.reject('ERR_INVALID_XML');
357
- }
358
- await checkStatus(samlContent, parserType, true);
359
- // Verify and decrypt SOAP response
360
- const [verified1, verifiedAssertionNode1, isDecryptRequired1] = await libsamlSoap.verifyAndDecryptSoapMessage(samlContent, verificationOptions);
361
- if (!verified1) {
362
- return Promise.reject('ERR_FAIL_TO_VERIFY_ETS_SIGNATURE');
363
- }
364
- samlContent = verifiedAssertionNode1;
365
- // Verify SAML signature and decrypt if needed
366
- const verificationResult = await libsaml.verifySignature(samlContent, verificationOptions, sp);
436
+ return {
437
+ resolveRequest,
438
+ resolved,
439
+ };
440
+ }
441
+ async function parseResolvedLoginRequestXml(params) {
442
+ let samlContent = params.samlContent;
443
+ const verificationOptions = {
444
+ metadata: params.sp.entityMeta,
445
+ signatureAlgorithm: params.sp.entitySetting.requestSignatureAlgorithm,
446
+ };
447
+ const signatureLooksPresent = /<[^>]*:?Signature\b/.test(samlContent);
448
+ if (params.idp.entityMeta.isWantAuthnRequestsSigned() || signatureLooksPresent) {
449
+ const verificationResult = await libsaml.verifySignature(samlContent, verificationOptions, params.idp);
367
450
  if (!verificationResult.status) {
368
451
  if (verificationResult.isMessageSigned && !verificationResult.MessageSignatureStatus) {
369
- return Promise.reject('ERR_FAIL_TO_VERIFY_MESSAGE_SIGNATURE');
452
+ fail('ERR_FAIL_TO_VERIFY_MESSAGE_SIGNATURE');
370
453
  }
371
454
  if (verificationResult.isAssertionSigned && !verificationResult.AssertionSignatureStatus) {
372
- return Promise.reject('ERR_FAIL_TO_VERIFY_ASSERTION_SIGNATURE');
455
+ fail('ERR_FAIL_TO_VERIFY_ASSERTION_SIGNATURE');
373
456
  }
374
- if (verificationResult.encrypted && !verificationResult.decrypted) {
375
- return Promise.reject('ERR_FAIL_TO_DECRYPT_ASSERTION');
376
- }
377
- return Promise.reject('ERR_FAIL_TO_VERIFY_SIGNATURE_OR_DECRYPTION');
457
+ fail('ERR_FAIL_TO_VERIFY_SIGNATURE_OR_DECRYPTION');
378
458
  }
379
459
  samlContent = verificationResult.samlContent;
380
460
  }
381
- // Extract fields
382
- let extractorFields;
383
- if (parserType === 'SAMLResponse') {
384
- extractorFields = getDefaultExtractorFields(parserType, samlContent);
385
- }
386
- else {
387
- extractorFields = getDefaultExtractorFields(parserType, null);
388
- }
389
461
  const parseResult = {
390
- samlContent: samlContent,
391
- extract: extract(samlContent, extractorFields),
462
+ samlContent,
463
+ extract: extract(samlContent, loginRequestFields),
464
+ };
465
+ if (parseResult.extract?.issuer !== params.sp.entityMeta.getEntityID()) {
466
+ fail('ERR_UNMATCH_ISSUER');
467
+ }
468
+ return parseResult;
469
+ }
470
+ async function parseLoginRequest(params) {
471
+ const { artifact, relayState } = getArtifactFromRequest(params.request);
472
+ validateArtifact(artifact, params.sp.entityMeta.getEntityID());
473
+ const { resolveRequest, resolved } = await resolveArtifact({
474
+ requester: params.idp,
475
+ responder: params.sp,
476
+ artifact,
477
+ });
478
+ const parseResult = await parseResolvedLoginRequestXml({
479
+ idp: params.idp,
480
+ sp: params.sp,
481
+ samlContent: resolved.samlContent,
482
+ });
483
+ ensureValidDestination(params.idp.entityMeta, 'singleSignOnService', parseResult?.extract?.request?.destination, binding.artifact, 'ERR_INVALID_DESTINATION');
484
+ ensureValidDestination(params.sp.entityMeta, 'assertionConsumerService', parseResult?.extract?.request?.assertionConsumerServiceUrl || parseResult?.extract?.request?.assertionConsumerServiceURL, undefined, 'ERR_INVALID_ASSERTION_CONSUMER_SERVICE');
485
+ return {
486
+ ...parseResult,
487
+ artifact,
488
+ relayState,
489
+ artifactResolve: {
490
+ request: resolveRequest,
491
+ response: resolved.extract,
492
+ },
493
+ };
494
+ }
495
+ async function parseLoginResponse(params) {
496
+ const { artifact, relayState } = getArtifactFromRequest(params.request);
497
+ validateArtifact(artifact, params.idp.entityMeta.getEntityID());
498
+ const { resolveRequest, resolved } = await resolveArtifact({
499
+ requester: params.sp,
500
+ responder: params.idp,
501
+ artifact,
502
+ });
503
+ const parseResult = await flow({
504
+ from: params.idp,
505
+ self: params.sp,
506
+ checkSignature: true,
507
+ parserType: ParserType.SAMLResponse,
508
+ type: 'login',
509
+ binding: binding.post,
510
+ request: {
511
+ body: {
512
+ SAMLResponse: utility.base64Encode(resolved.samlContent),
513
+ },
514
+ },
515
+ });
516
+ return {
517
+ ...parseResult,
518
+ artifact,
519
+ relayState,
520
+ artifactResolve: {
521
+ request: resolveRequest,
522
+ response: resolved.extract,
523
+ },
392
524
  };
393
- // Validation
394
- const targetEntityMetadata = idp.entityMeta;
395
- const issuer = targetEntityMetadata.getEntityID();
396
- const extractedProperties = parseResult.extract;
397
- // Check issuer
398
- if (parserType === ParserType.SAMLResponse
399
- && extractedProperties
400
- && extractedProperties.issuer !== issuer) {
401
- return Promise.reject('ERR_UNMATCH_ISSUER');
402
- }
403
- // Check session time
404
- if (parserType === 'SAMLResponse'
405
- && extractedProperties.sessionIndex.sessionNotOnOrAfter
406
- && !verifyTime(undefined, extractedProperties.sessionIndex.sessionNotOnOrAfter, sp.entitySetting.clockDrifts)) {
407
- return Promise.reject('ERR_EXPIRED_SESSION');
408
- }
409
- // Check conditions time
410
- if (parserType === 'SAMLResponse'
411
- && extractedProperties.conditions
412
- && !verifyTime(extractedProperties.conditions.notBefore, extractedProperties.conditions.notOnOrAfter, sp.entitySetting.clockDrifts)) {
413
- return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
414
- }
415
- return Promise.resolve(parseResult);
416
525
  }
526
+ export const generateArtifactId = generateArtifactIdUtil;
417
527
  const artifactBinding = {
418
- soapLoginRequest,
419
- soapLoginResponse,
420
- parseLoginRequestResolve,
421
- parseLoginResponseResolve,
528
+ createLoginRequest,
529
+ createLoginResponse,
530
+ parseLoginRequest,
531
+ parseLoginResponse,
532
+ createArtifactResolveRequest,
533
+ parseArtifactResolveRequest,
534
+ createArtifactResolveResponse,
535
+ parseArtifactResolveResponse,
422
536
  generateArtifactId,
423
537
  };
424
538
  export default artifactBinding;