samlesa 3.5.0 → 4.1.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/build/index.js CHANGED
@@ -13,6 +13,8 @@ import * as Soap from './src/soap.js';
13
13
  import { validate, validateMetadata } from './src/schemaValidator.js';
14
14
  // exposed methods for customizing samlify
15
15
  import { setSchemaValidator, setDOMParserOptions } from './src/api.js';
16
+ // SAML 2.0 enhancements
17
+ export * from './src/saml2-enhancements.js';
16
18
  export { Constants, Extractor,
17
19
  // temp: resolve the conflict after version >= 3.0
18
20
  IdentityProvider, IdentityProviderInstance, ServiceProvider, ServiceProviderInstance,
@@ -15,6 +15,7 @@ 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
+ import { applyAuthnRequestEnhancements } from './saml2-enhancements-integration.js';
18
19
  const binding = wording.binding;
19
20
  /**
20
21
  * Get default extractor fields based on parser type
@@ -99,6 +100,10 @@ function soapLoginRequest(referenceTagXPath, entity, customTagReplacement) {
99
100
  NameIDFormat: selectedNameIDFormat
100
101
  });
101
102
  }
103
+ // 应用 AuthnRequest 增强功能
104
+ if (spSetting.authnRequestEnhancements) {
105
+ rawSamlRequest = applyAuthnRequestEnhancements(rawSamlRequest, spSetting.authnRequestEnhancements);
106
+ }
102
107
  const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
103
108
  let signedAuthnRequest;
104
109
  if (metadata.idp.isWantAuthnRequestsSigned()) {
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * @file binding-post.ts
3
- * @author tngan
4
3
  * @desc Binding-level API, declare the functions using POST binding
5
4
  */
6
5
  import { wording, namespace, StatusCode } from './urn.js';
7
6
  import { randomUUID } from 'node:crypto';
8
7
  import libsaml from './libsaml.js';
9
8
  import utility, { get } from './utility.js';
9
+ import { applyAuthnRequestEnhancements } from './saml2-enhancements-integration.js';
10
10
  const binding = wording.binding;
11
11
  /**
12
12
  * @desc Generate a base64 encoded login request
@@ -41,6 +41,10 @@ function base64LoginRequest(referenceTagXPath, entity, customTagReplacement) {
41
41
  NameIDFormat: selectedNameIDFormat
42
42
  });
43
43
  }
44
+ // 应用 AuthnRequest 增强功能(包括 ForceAuthn, IsPassive, Consent, ProviderName 等)
45
+ if (spSetting.authnRequestEnhancements) {
46
+ rawSamlRequest = applyAuthnRequestEnhancements(rawSamlRequest, spSetting.authnRequestEnhancements);
47
+ }
44
48
  if (metadata.idp.isWantAuthnRequestsSigned()) {
45
49
  const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
46
50
  return {
@@ -6,6 +6,7 @@
6
6
  import utility, { get } from './utility.js';
7
7
  import libsaml from './libsaml.js';
8
8
  import { namespace, wording } from './urn.js';
9
+ import { applyAuthnRequestEnhancements } from './saml2-enhancements-integration.js';
9
10
  const binding = wording.binding;
10
11
  const urlParams = wording.urlParams;
11
12
  /**
@@ -87,6 +88,10 @@ function loginRequestRedirectURL(entity, customTagReplacement) {
87
88
  AllowCreate: spSetting.allowCreate,
88
89
  });
89
90
  }
91
+ // 应用 AuthnRequest 增强功能(包括 ForceAuthn, IsPassive, Consent, ProviderName 等)
92
+ if (spSetting.authnRequestEnhancements) {
93
+ rawSamlRequest = applyAuthnRequestEnhancements(rawSamlRequest, spSetting.authnRequestEnhancements);
94
+ }
90
95
  return {
91
96
  id,
92
97
  context: buildRedirectURL({
@@ -124,6 +129,12 @@ function loginRequestRedirectURLArt(entity, customTagReplacement) {
124
129
  const nameIDFormat = spSetting.nameIDFormat;
125
130
  const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat;
126
131
  id = spSetting.generateID();
132
+ // 构建 AuthnRequest 增强属性
133
+ const authnEnhancements = spSetting.authnRequestEnhancements || {};
134
+ const forceAuthn = authnEnhancements.forceAuthn !== undefined ? String(authnEnhancements.forceAuthn) : '';
135
+ const isPassive = authnEnhancements.isPassive !== undefined ? String(authnEnhancements.isPassive) : '';
136
+ const consent = authnEnhancements.consent || '';
137
+ const providerName = authnEnhancements.providerName || '';
127
138
  rawSamlRequest = libsaml.replaceTagsByValue(libsaml.defaultLoginRequestTemplate.context, {
128
139
  ID: id,
129
140
  Destination: base,
@@ -133,6 +144,10 @@ function loginRequestRedirectURLArt(entity, customTagReplacement) {
133
144
  AssertionConsumerServiceURL: metadata.sp.getAssertionConsumerService(binding.post),
134
145
  EntityID: metadata.sp.getEntityID(),
135
146
  AllowCreate: spSetting.allowCreate,
147
+ ForceAuthn: forceAuthn,
148
+ IsPassive: isPassive,
149
+ Consent: consent,
150
+ ProviderName: providerName
136
151
  });
137
152
  }
138
153
  const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm, transformationAlgorithms } = spSetting;
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { X509Certificate } from 'node:crypto';
7
7
  import xml from 'xml';
8
- import utility, { flattenDeep, inflateString, isString } from './utility.js';
8
+ import utility, { inflateString, isString, normalizeCertificates } from './utility.js';
9
9
  ;
10
10
  import * as crypto from 'node:crypto';
11
11
  import { algorithms, namespace, wording } from './urn.js';
@@ -524,13 +524,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
524
524
  else if (opts.metadata) {
525
525
  const certificateNode = select(".//*[local-name() = 'X509Certificate']", signatureNode);
526
526
  let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
527
- if (Array.isArray(metadataCert)) {
528
- metadataCert = flattenDeep(metadataCert);
529
- }
530
- else if (typeof metadataCert === 'string') {
531
- metadataCert = [metadataCert];
532
- }
533
- metadataCert = metadataCert.map(utility.normalizeCerString);
527
+ metadataCert = normalizeCertificates(metadataCert);
534
528
  if (certificateNode.length === 0 && metadataCert.length === 0) {
535
529
  throw new Error('NO_SELECTED_CERTIFICATE');
536
530
  }
@@ -581,13 +575,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
581
575
  else if (opts.metadata) {
582
576
  const certificateNode = select(".//*[local-name() = 'X509Certificate']", signatureNode);
583
577
  let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
584
- if (Array.isArray(metadataCert)) {
585
- metadataCert = flattenDeep(metadataCert);
586
- }
587
- else if (typeof metadataCert === 'string') {
588
- metadataCert = [metadataCert];
589
- }
590
- metadataCert = metadataCert.map(utility.normalizeCerString);
578
+ metadataCert = normalizeCertificates(metadataCert);
591
579
  if (certificateNode.length === 0 && metadataCert.length === 0) {
592
580
  throw new Error('NO_SELECTED_CERTIFICATE');
593
581
  }
@@ -627,13 +615,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
627
615
  else if (opts.metadata) {
628
616
  const certificateNode = select(".//*[local-name() = 'X509Certificate']", signatureNode);
629
617
  let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
630
- if (Array.isArray(metadataCert)) {
631
- metadataCert = flattenDeep(metadataCert);
632
- }
633
- else if (typeof metadataCert === 'string') {
634
- metadataCert = [metadataCert];
635
- }
636
- metadataCert = metadataCert.map(utility.normalizeCerString);
618
+ metadataCert = normalizeCertificates(metadataCert);
637
619
  if (certificateNode.length === 0 && metadataCert.length === 0) {
638
620
  throw new Error('NO_SELECTED_CERTIFICATE');
639
621
  }
@@ -791,13 +773,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
791
773
  const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
792
774
  // 证书处理逻辑
793
775
  let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
794
- if (Array.isArray(metadataCert)) {
795
- metadataCert = flattenDeep(metadataCert);
796
- }
797
- else if (typeof metadataCert === 'string') {
798
- metadataCert = [metadataCert];
799
- }
800
- metadataCert = metadataCert.map(utility.normalizeCerString);
776
+ metadataCert = normalizeCertificates(metadataCert);
801
777
  // 没有证书的情况
802
778
  if (certificateNode.length === 0 && metadataCert.length === 0) {
803
779
  throw new Error('NO_SELECTED_CERTIFICATE');
@@ -1070,13 +1046,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
1070
1046
  else if (opts.metadata) {
1071
1047
  const certificateNode = select(".//*[local-name(.)='X509Certificate']", signatureNode);
1072
1048
  let metadataCert = opts.metadata.getX509Certificate(certUse.signing);
1073
- if (Array.isArray(metadataCert)) {
1074
- metadataCert = flattenDeep(metadataCert);
1075
- }
1076
- else if (typeof metadataCert === 'string') {
1077
- metadataCert = [metadataCert];
1078
- }
1079
- metadataCert = metadataCert.map(utility.normalizeCerString);
1049
+ metadataCert = normalizeCertificates(metadataCert);
1080
1050
  if (certificateNode.length === 0 && metadataCert.length === 0) {
1081
1051
  throw new Error('NO_SELECTED_CERTIFICATE');
1082
1052
  }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @file saml2-enhancements-integration.ts
3
+ * @desc SAML 2.0 增强功能集成 - 自动应用到绑定和元数据生成
4
+ */
5
+ import { buildScoping, buildRequestedAuthnContext, buildOneTimeUse, buildProxyRestriction, buildOrganization, buildContactPerson, xmlToString, AuthnContextClassRef, Consent, } from './saml2-enhancements.js';
6
+ import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
7
+ /**
8
+ * 将增强功能应用到 AuthnRequest
9
+ */
10
+ export function applyAuthnRequestEnhancements(rawSamlRequest, enhancements) {
11
+ const doc = new DOMParser().parseFromString(rawSamlRequest, 'application/xml');
12
+ const authnRequestElement = doc.documentElement;
13
+ if (!authnRequestElement) {
14
+ throw new Error('Invalid AuthnRequest XML');
15
+ }
16
+ // 1. 添加可选属性(仅当配置了值时)
17
+ if (enhancements.forceAuthn !== undefined) {
18
+ authnRequestElement.setAttribute('ForceAuthn', String(enhancements.forceAuthn));
19
+ }
20
+ if (enhancements.isPassive !== undefined) {
21
+ authnRequestElement.setAttribute('IsPassive', String(enhancements.isPassive));
22
+ }
23
+ if (enhancements.consent) {
24
+ authnRequestElement.setAttribute('Consent', enhancements.consent);
25
+ }
26
+ if (enhancements.attributeConsumingServiceIndex !== undefined) {
27
+ authnRequestElement.setAttribute('AttributeConsumingServiceIndex', String(enhancements.attributeConsumingServiceIndex));
28
+ }
29
+ if (enhancements.providerName) {
30
+ authnRequestElement.setAttribute('ProviderName', enhancements.providerName);
31
+ }
32
+ // 2. 添加 Scoping 元素
33
+ if (enhancements.scoping) {
34
+ const scopingElement = buildScopingElement(enhancements.scoping, doc);
35
+ authnRequestElement.appendChild(scopingElement);
36
+ }
37
+ // 3. 添加 RequestedAuthnContext 元素
38
+ if (enhancements.requestedAuthnContext) {
39
+ const contextElement = buildRequestedAuthnContextElement(enhancements.requestedAuthnContext, doc);
40
+ authnRequestElement.appendChild(contextElement);
41
+ }
42
+ return new XMLSerializer().serializeToString(doc);
43
+ }
44
+ /**
45
+ * 将增强功能应用到 Conditions 元素
46
+ */
47
+ export function applyConditionsEnhancements(rawSamlResponse, enhancements) {
48
+ const doc = new DOMParser().parseFromString(rawSamlResponse, 'application/xml');
49
+ const conditionsElements = doc.getElementsByTagName('saml:Conditions');
50
+ if (conditionsElements.length > 0) {
51
+ const conditionsElement = conditionsElements[0];
52
+ // 1. 添加 OneTimeUse
53
+ if (enhancements.oneTimeUse?.enabled) {
54
+ const oneTimeUseElement = buildOneTimeUseElement(doc);
55
+ conditionsElement.appendChild(oneTimeUseElement);
56
+ }
57
+ // 2. 添加 ProxyRestriction
58
+ if (enhancements.proxyRestriction) {
59
+ const proxyRestrictionElement = buildProxyRestrictionElement(enhancements.proxyRestriction, doc);
60
+ conditionsElement.appendChild(proxyRestrictionElement);
61
+ }
62
+ }
63
+ return new XMLSerializer().serializeToString(doc);
64
+ }
65
+ /**
66
+ * 将增强功能应用到 SubjectConfirmationData 元素
67
+ */
68
+ export function applySubjectConfirmationEnhancements(rawSamlResponse, enhancements) {
69
+ const doc = new DOMParser().parseFromString(rawSamlResponse, 'application/xml');
70
+ const subjectConfirmationDataElements = doc.getElementsByTagName('saml:SubjectConfirmationData');
71
+ if (subjectConfirmationDataElements.length > 0) {
72
+ const element = subjectConfirmationDataElements[0];
73
+ if (enhancements.address) {
74
+ element.setAttribute('Address', enhancements.address);
75
+ }
76
+ if (enhancements.notBefore) {
77
+ element.setAttribute('NotBefore', enhancements.notBefore);
78
+ }
79
+ if (enhancements.inResponseTo) {
80
+ element.setAttribute('InResponseTo', enhancements.inResponseTo);
81
+ }
82
+ }
83
+ return new XMLSerializer().serializeToString(doc);
84
+ }
85
+ /**
86
+ * 将增强功能应用到元数据
87
+ */
88
+ export function applyMetadataEnhancements(rawMetadata, enhancements) {
89
+ const doc = new DOMParser().parseFromString(rawMetadata, 'application/xml');
90
+ const entityDescriptor = doc.documentElement;
91
+ if (!entityDescriptor) {
92
+ throw new Error('Invalid Metadata XML');
93
+ }
94
+ // 1. 添加 Organization
95
+ if (enhancements.organization) {
96
+ const orgElement = buildOrganizationElement(enhancements.organization, doc);
97
+ entityDescriptor.appendChild(orgElement);
98
+ }
99
+ // 2. 添加 ContactPerson
100
+ if (enhancements.contactPerson && enhancements.contactPerson.length > 0) {
101
+ enhancements.contactPerson.forEach(contact => {
102
+ const contactElement = buildContactPersonElement(contact, doc);
103
+ entityDescriptor.appendChild(contactElement);
104
+ });
105
+ }
106
+ return new XMLSerializer().serializeToString(doc);
107
+ }
108
+ // ============================================================================
109
+ // 辅助函数:构建 DOM 元素
110
+ // ============================================================================
111
+ function buildScopingElement(config, doc) {
112
+ const scoping = buildScoping(config);
113
+ return xmlObjectToElement(scoping, doc, 'samlp');
114
+ }
115
+ function buildRequestedAuthnContextElement(config, doc) {
116
+ const context = buildRequestedAuthnContext(config);
117
+ return xmlObjectToElement(context, doc, 'samlp');
118
+ }
119
+ function buildOneTimeUseElement(doc) {
120
+ const oneTimeUse = buildOneTimeUse();
121
+ return xmlObjectToElement(oneTimeUse, doc, 'saml');
122
+ }
123
+ function buildProxyRestrictionElement(config, doc) {
124
+ const proxyRestriction = buildProxyRestriction(config);
125
+ return xmlObjectToElement(proxyRestriction, doc, 'saml');
126
+ }
127
+ function buildOrganizationElement(config, doc) {
128
+ const organization = buildOrganization(config);
129
+ return xmlObjectToElement(organization, doc, 'md');
130
+ }
131
+ function buildContactPersonElement(config, doc) {
132
+ const contactPerson = buildContactPerson(config);
133
+ return xmlObjectToElement(contactPerson, doc, 'md');
134
+ }
135
+ // ============================================================================
136
+ // 工具函数:将 XML 对象转换为 DOM 元素
137
+ // ============================================================================
138
+ function xmlObjectToElement(xmlObj, doc, defaultNs) {
139
+ const xmlStr = xmlToString(xmlObj);
140
+ const tempDoc = new DOMParser().parseFromString(xmlStr, 'application/xml');
141
+ return doc.importNode(tempDoc.documentElement, true);
142
+ }
143
+ // ============================================================================
144
+ // 导出常量
145
+ // ============================================================================
146
+ export { AuthnContextClassRef, Consent };