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 +2 -0
- package/build/src/binding-artifact.js +5 -0
- package/build/src/binding-post.js +5 -1
- package/build/src/binding-redirect.js +15 -0
- package/build/src/libsaml.js +6 -36
- package/build/src/saml2-enhancements-integration.js +146 -0
- package/build/src/saml2-enhancements.js +405 -0
- package/build/src/urn.js +4 -0
- package/build/src/utility.js +132 -3
- package/package.json +1 -1
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
- package/types/src/binding-artifact.d.ts.map +1 -1
- package/types/src/binding-post.d.ts +0 -1
- package/types/src/binding-post.d.ts.map +1 -1
- package/types/src/binding-redirect.d.ts.map +1 -1
- package/types/src/libsaml.d.ts.map +1 -1
- package/types/src/saml2-enhancements-integration.d.ts +24 -0
- package/types/src/saml2-enhancements-integration.d.ts.map +1 -0
- package/types/src/saml2-enhancements.d.ts +286 -0
- package/types/src/saml2-enhancements.d.ts.map +1 -0
- package/types/src/types.d.ts +105 -0
- package/types/src/types.d.ts.map +1 -1
- package/types/src/urn.d.ts +4 -0
- package/types/src/urn.d.ts.map +1 -1
- package/types/src/utility.d.ts +18 -0
- package/types/src/utility.d.ts.map +1 -1
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;
|
package/build/src/libsaml.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { X509Certificate } from 'node:crypto';
|
|
7
7
|
import xml from 'xml';
|
|
8
|
-
import utility, {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|