samlesa 4.3.1 → 4.3.3
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/src/binding-redirect.js +4 -1
- package/build/src/binding-simplesign.js +4 -1
- package/build/src/entity-sp.js +2 -1
- package/build/src/entity.js +6 -0
- package/build/src/flow.js +8 -2
- package/build/src/libsaml.js +71 -12
- package/build/src/metadata-idp.js +4 -1
- package/build/src/metadata-sp.js +138 -167
- package/build/src/schemaValidator.js +64 -125
- package/package.json +87 -87
- package/types/src/binding-redirect.d.ts.map +1 -1
- package/types/src/binding-simplesign.d.ts.map +1 -1
- package/types/src/entity-sp.d.ts.map +1 -1
- package/types/src/entity.d.ts.map +1 -1
- package/types/src/flow.d.ts.map +1 -1
- package/types/src/libsaml.d.ts +10 -4
- package/types/src/libsaml.d.ts.map +1 -1
- package/types/src/metadata-idp.d.ts.map +1 -1
- package/types/src/metadata-sp.d.ts +8 -1
- package/types/src/metadata-sp.d.ts.map +1 -1
- package/types/src/schemaValidator.d.ts +1 -15
- package/types/src/schemaValidator.d.ts.map +1 -1
- package/types/src/types.d.ts +8 -0
- package/types/src/types.d.ts.map +1 -1
|
@@ -50,7 +50,10 @@ function buildRedirectURL(opts) {
|
|
|
50
50
|
const octetString = samlRequest + relayState + sigAlg;
|
|
51
51
|
return baseUrl
|
|
52
52
|
+ pvPair(queryParam, octetString, noParams)
|
|
53
|
-
+ pvPair(urlParams.signature, encodeURIComponent(libsaml.constructMessageSignature(queryParam + '=' + octetString, entitySetting.privateKey, entitySetting.privateKeyPass, undefined, entitySetting.requestSignatureAlgorithm
|
|
53
|
+
+ pvPair(urlParams.signature, encodeURIComponent(libsaml.constructMessageSignature(queryParam + '=' + octetString, entitySetting.privateKey, entitySetting.privateKeyPass, undefined, entitySetting.requestSignatureAlgorithm, {
|
|
54
|
+
strictSecurity: entitySetting.strictSecurity,
|
|
55
|
+
allowLegacySha1: entitySetting.allowLegacySha1,
|
|
56
|
+
}).toString()));
|
|
54
57
|
}
|
|
55
58
|
return baseUrl + pvPair(queryParam, samlRequest + relayState, noParams);
|
|
56
59
|
}
|
|
@@ -36,7 +36,10 @@ function buildSimpleSignature(opts) {
|
|
|
36
36
|
}
|
|
37
37
|
const sigAlg = pvPair(urlParams.sigAlg, entitySetting.requestSignatureAlgorithm);
|
|
38
38
|
const octetString = context + relayState + sigAlg;
|
|
39
|
-
return libsaml.constructMessageSignature(queryParam + '=' + octetString, entitySetting.privateKey, entitySetting.privateKeyPass, undefined, entitySetting.requestSignatureAlgorithm
|
|
39
|
+
return libsaml.constructMessageSignature(queryParam + '=' + octetString, entitySetting.privateKey, entitySetting.privateKeyPass, undefined, entitySetting.requestSignatureAlgorithm, {
|
|
40
|
+
strictSecurity: entitySetting.strictSecurity,
|
|
41
|
+
allowLegacySha1: entitySetting.allowLegacySha1,
|
|
42
|
+
}).toString();
|
|
40
43
|
}
|
|
41
44
|
/**
|
|
42
45
|
* @desc Generate a base64 encoded login request
|
package/build/src/entity-sp.js
CHANGED
|
@@ -42,7 +42,8 @@ export class ServiceProvider extends Entity {
|
|
|
42
42
|
createLoginRequest(idp, binding = 'redirect', customTagReplacement) {
|
|
43
43
|
const nsBinding = namespace.binding;
|
|
44
44
|
const protocol = nsBinding[binding];
|
|
45
|
-
|
|
45
|
+
const strictSecurity = this.entitySetting.strictSecurity !== false;
|
|
46
|
+
if (strictSecurity && this.entityMeta.isAuthnRequestSigned() !== idp.entityMeta.isWantAuthnRequestsSigned()) {
|
|
46
47
|
throw new Error('ERR_METADATA_CONFLICT_REQUEST_SIGNED_FLAG');
|
|
47
48
|
}
|
|
48
49
|
let context = null;
|
package/build/src/entity.js
CHANGED
|
@@ -16,6 +16,8 @@ const keyEncryptionAlgorithm = algorithms.encryption.key;
|
|
|
16
16
|
const signatureAlgorithms = algorithms.signature;
|
|
17
17
|
const messageSigningOrders = messageConfigurations.signingOrder;
|
|
18
18
|
const defaultEntitySetting = {
|
|
19
|
+
strictSecurity: true,
|
|
20
|
+
allowLegacySha1: false,
|
|
19
21
|
wantLogoutResponseSigned: false,
|
|
20
22
|
messageSigningOrder: messageSigningOrders.SIGN_THEN_ENCRYPT,
|
|
21
23
|
wantLogoutRequestSigned: false,
|
|
@@ -34,6 +36,10 @@ export default class Entity {
|
|
|
34
36
|
*/
|
|
35
37
|
constructor(entitySetting, entityType) {
|
|
36
38
|
this.entitySetting = Object.assign({}, defaultEntitySetting, entitySetting);
|
|
39
|
+
const rawEntitySetting = entitySetting;
|
|
40
|
+
if (this.entitySetting.strictSecurity === false && rawEntitySetting.allowLegacySha1 === undefined) {
|
|
41
|
+
this.entitySetting.allowLegacySha1 = true;
|
|
42
|
+
}
|
|
37
43
|
const metadata = entitySetting.metadata || entitySetting;
|
|
38
44
|
switch (entityType) {
|
|
39
45
|
case 'idp':
|
package/build/src/flow.js
CHANGED
|
@@ -77,7 +77,10 @@ async function redirectFlow(options) {
|
|
|
77
77
|
// put the below two assignments into verifyMessageSignature function
|
|
78
78
|
const base64Signature = Buffer.from(decodeURIComponent(signature), 'base64');
|
|
79
79
|
const decodeSigAlg = decodeURIComponent(sigAlg);
|
|
80
|
-
const verified = libsaml.verifyMessageSignature(targetEntityMetadata, octetString, base64Signature, sigAlg
|
|
80
|
+
const verified = libsaml.verifyMessageSignature(targetEntityMetadata, octetString, base64Signature, sigAlg, {
|
|
81
|
+
strictSecurity: self?.entitySetting?.strictSecurity,
|
|
82
|
+
allowLegacySha1: self?.entitySetting?.allowLegacySha1,
|
|
83
|
+
});
|
|
81
84
|
if (!verified) {
|
|
82
85
|
// Fail to verify message signature
|
|
83
86
|
return Promise.reject('ERR_FAILED_MESSAGE_SIGNATURE_VERIFICATION');
|
|
@@ -478,7 +481,10 @@ async function postSimpleSignFlow(options) {
|
|
|
478
481
|
}
|
|
479
482
|
// put the below two assignments into verifyMessageSignature function
|
|
480
483
|
const base64Signature = Buffer.from(signature, 'base64');
|
|
481
|
-
const verified = libsaml.verifyMessageSignature(targetEntityMetadata, octetString, base64Signature, sigAlg
|
|
484
|
+
const verified = libsaml.verifyMessageSignature(targetEntityMetadata, octetString, base64Signature, sigAlg, {
|
|
485
|
+
strictSecurity: self?.entitySetting?.strictSecurity,
|
|
486
|
+
allowLegacySha1: self?.entitySetting?.allowLegacySha1,
|
|
487
|
+
});
|
|
482
488
|
if (!verified) {
|
|
483
489
|
// Fail to verify message signature
|
|
484
490
|
return Promise.reject('ERR_FAILED_MESSAGE_SIGNATURE_VERIFICATION');
|
package/build/src/libsaml.js
CHANGED
|
@@ -67,6 +67,44 @@ const libSaml = () => {
|
|
|
67
67
|
unsafeSignatureAlgorithm: isUnsafe ? signatureAlgorithm : null
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
+
function getEnvironmentBoolean(name) {
|
|
71
|
+
const rawValue = process.env[name];
|
|
72
|
+
if (rawValue === undefined) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const normalized = rawValue.trim().toLowerCase();
|
|
76
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
function resolveAllowLegacySha1(securityOptions, self) {
|
|
85
|
+
if (securityOptions?.allowLegacySha1 !== undefined) {
|
|
86
|
+
return securityOptions.allowLegacySha1;
|
|
87
|
+
}
|
|
88
|
+
if (securityOptions?.strictSecurity !== undefined) {
|
|
89
|
+
return securityOptions.strictSecurity === false;
|
|
90
|
+
}
|
|
91
|
+
const envAllowLegacy = getEnvironmentBoolean('SAMLIFY_ALLOW_LEGACY_SHA1');
|
|
92
|
+
if (envAllowLegacy !== undefined) {
|
|
93
|
+
return envAllowLegacy;
|
|
94
|
+
}
|
|
95
|
+
const envStrictSecurity = getEnvironmentBoolean('SAMLIFY_STRICT_SECURITY');
|
|
96
|
+
if (envStrictSecurity !== undefined) {
|
|
97
|
+
return envStrictSecurity === false;
|
|
98
|
+
}
|
|
99
|
+
const entitySetting = self?.entitySetting;
|
|
100
|
+
if (entitySetting?.allowLegacySha1 !== undefined) {
|
|
101
|
+
return entitySetting.allowLegacySha1 === true;
|
|
102
|
+
}
|
|
103
|
+
if (entitySetting?.strictSecurity !== undefined) {
|
|
104
|
+
return entitySetting.strictSecurity === false;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
70
108
|
/**
|
|
71
109
|
* @desc Default login request template
|
|
72
110
|
* @type {LoginRequestTemplate}
|
|
@@ -166,14 +204,17 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
166
204
|
'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384': 'sha384',
|
|
167
205
|
'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512': 'sha512',
|
|
168
206
|
};
|
|
169
|
-
function getSigningAlgorithm(sigAlg) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
207
|
+
function getSigningAlgorithm(sigAlg, securityOptions, self) {
|
|
208
|
+
const algorithm = sigAlg ?? signatureAlgorithms.RSA_SHA256;
|
|
209
|
+
const safetyCheck = checkUnsafeSignatureAlgorithm(algorithm);
|
|
210
|
+
if (safetyCheck.hasUnsafeSignatureAlgorithm && !resolveAllowLegacySha1(securityOptions, self)) {
|
|
211
|
+
throw new Error('ERR_UNSAFE_SIGNATURE_ALGORITHM');
|
|
212
|
+
}
|
|
213
|
+
const algAlias = cryptoAlgorithmMapping[algorithm];
|
|
214
|
+
if (algAlias !== undefined) {
|
|
215
|
+
return algAlias;
|
|
175
216
|
}
|
|
176
|
-
|
|
217
|
+
throw new Error('ERR_UNSUPPORTED_SIGNATURE_ALGORITHM');
|
|
177
218
|
}
|
|
178
219
|
function validateAndInflateSamlResponse(urlEncodedResponse) {
|
|
179
220
|
// 3. 尝试DEFLATE解压(SAML规范要求使用原始DEFLATE)
|
|
@@ -514,6 +555,9 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
514
555
|
const checkResult = checkUnsafeSignatureAlgorithm(signatureAlgorithm);
|
|
515
556
|
hasUnsafeSignatureAlgorithm = checkResult.hasUnsafeSignatureAlgorithm;
|
|
516
557
|
unsafeSignatureAlgorithm = checkResult.unsafeSignatureAlgorithm ?? "";
|
|
558
|
+
if (checkResult.hasUnsafeSignatureAlgorithm && !resolveAllowLegacySha1(opts, self)) {
|
|
559
|
+
throw new Error('ERR_UNSAFE_SIGNATURE_ALGORITHM');
|
|
560
|
+
}
|
|
517
561
|
const sig = new SignedXml();
|
|
518
562
|
if (!opts.keyFile && !opts.metadata) {
|
|
519
563
|
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
@@ -565,6 +609,9 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
565
609
|
const checkResult = checkUnsafeSignatureAlgorithm(signatureAlgorithm);
|
|
566
610
|
hasUnsafeSignatureAlgorithm = checkResult.hasUnsafeSignatureAlgorithm;
|
|
567
611
|
unsafeSignatureAlgorithm = checkResult.unsafeSignatureAlgorithm ?? "";
|
|
612
|
+
if (checkResult.hasUnsafeSignatureAlgorithm && !resolveAllowLegacySha1(opts, self)) {
|
|
613
|
+
throw new Error('ERR_UNSAFE_SIGNATURE_ALGORITHM');
|
|
614
|
+
}
|
|
568
615
|
const sig = new SignedXml();
|
|
569
616
|
if (!opts.keyFile && !opts.metadata) {
|
|
570
617
|
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
@@ -605,6 +652,9 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
605
652
|
const checkResult = checkUnsafeSignatureAlgorithm(signatureAlgorithm);
|
|
606
653
|
hasUnsafeSignatureAlgorithm = checkResult.hasUnsafeSignatureAlgorithm;
|
|
607
654
|
unsafeSignatureAlgorithm = checkResult.unsafeSignatureAlgorithm ?? "";
|
|
655
|
+
if (checkResult.hasUnsafeSignatureAlgorithm && !resolveAllowLegacySha1(opts, self)) {
|
|
656
|
+
throw new Error('ERR_UNSAFE_SIGNATURE_ALGORITHM');
|
|
657
|
+
}
|
|
608
658
|
const sig = new SignedXml();
|
|
609
659
|
if (!opts.keyFile && !opts.metadata) {
|
|
610
660
|
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
@@ -762,6 +812,9 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
762
812
|
let verified = false;
|
|
763
813
|
// 检测不安全的签名算法
|
|
764
814
|
const { hasUnsafeSignatureAlgorithm, unsafeSignatureAlgorithm } = checkUnsafeSignatureAlgorithm(opts.signatureAlgorithm || '');
|
|
815
|
+
if (hasUnsafeSignatureAlgorithm && !resolveAllowLegacySha1(opts)) {
|
|
816
|
+
throw new Error('ERR_UNSAFE_SIGNATURE_ALGORITHM');
|
|
817
|
+
}
|
|
765
818
|
sig.signatureAlgorithm = opts.signatureAlgorithm;
|
|
766
819
|
if (!opts.keyFile && !opts.metadata) {
|
|
767
820
|
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
@@ -866,9 +919,9 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
866
919
|
* @param signingAlgorithm - 签名算法 (默认 'rsa-sha256')
|
|
867
920
|
* @returns 消息签名
|
|
868
921
|
*/
|
|
869
|
-
constructMessageSignature(octetString, key, passphrase, isBase64, signingAlgorithm) {
|
|
922
|
+
constructMessageSignature(octetString, key, passphrase, isBase64, signingAlgorithm, securityOptions) {
|
|
870
923
|
try {
|
|
871
|
-
const algorithm = getSigningAlgorithm(signingAlgorithm ?? signatureAlgorithms.RSA_SHA256);
|
|
924
|
+
const algorithm = getSigningAlgorithm(signingAlgorithm ?? signatureAlgorithms.RSA_SHA256, securityOptions);
|
|
872
925
|
const privateKeyPem = utility.readPrivateKey(key, passphrase); // 假设utility对象存在
|
|
873
926
|
const signer = crypto.createSign(algorithm);
|
|
874
927
|
signer.update(octetString, 'utf8');
|
|
@@ -893,10 +946,10 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
893
946
|
* @return {boolean} verification result
|
|
894
947
|
*/
|
|
895
948
|
verifyMessageSignature(metadata, // 假设metadata对象有getX509Certificate方法
|
|
896
|
-
octetString, signature, verifyAlgorithm) {
|
|
949
|
+
octetString, signature, verifyAlgorithm, securityOptions) {
|
|
897
950
|
try {
|
|
898
951
|
const signCert = metadata.getX509Certificate('signing'); // 假设certUse.signing是'signing'
|
|
899
|
-
const algorithm = getSigningAlgorithm(verifyAlgorithm);
|
|
952
|
+
const algorithm = getSigningAlgorithm(verifyAlgorithm, securityOptions);
|
|
900
953
|
const publicKeyPem = utility.getPublicKeyPemFromCertificate(signCert); // 假设utility对象存在
|
|
901
954
|
const verifier = crypto.createVerify(algorithm);
|
|
902
955
|
verifier.update(octetString, 'utf8');
|
|
@@ -960,7 +1013,7 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
960
1013
|
pem: Buffer.from(`-----BEGIN CERTIFICATE-----${encryptPem}-----END CERTIFICATE-----`),
|
|
961
1014
|
encryptionAlgorithm: sourceEntitySetting.dataEncryptionAlgorithm,
|
|
962
1015
|
keyEncryptionAlgorithm: sourceEntitySetting.keyEncryptionAlgorithm,
|
|
963
|
-
keyEncryptionDigest: sourceEntitySetting.keyEncryptionDigest ?? '
|
|
1016
|
+
keyEncryptionDigest: sourceEntitySetting.keyEncryptionDigest ?? 'sha256', // default sha256
|
|
964
1017
|
keyEncryptionMgf1: sourceEntitySetting.keyEncryptionMgf1 ?? 'sha256',
|
|
965
1018
|
disallowEncryptionWithInsecureAlgorithm: sourceEntitySetting.disallowEncryptionWithInsecureAlgorithm, // 禁止使用rsa-1_5 tripledes-cbc
|
|
966
1019
|
disallowInsecureEncryption: sourceEntitySetting.disallowInsecureEncryption, //禁aes cbc系列加密算法
|
|
@@ -1031,6 +1084,9 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
1031
1084
|
let checkResult = checkUnsafeSignatureAlgorithm(signatureAlgorithm.value || '');
|
|
1032
1085
|
hasUnsafeSignatureAlgorithm = checkResult.hasUnsafeSignatureAlgorithm;
|
|
1033
1086
|
unsafeSignatureAlgorithm = checkResult.unsafeSignatureAlgorithm ?? "";
|
|
1087
|
+
if (checkResult.hasUnsafeSignatureAlgorithm && !resolveAllowLegacySha1(opts, here)) {
|
|
1088
|
+
throw new Error('ERR_UNSAFE_SIGNATURE_ALGORITHM');
|
|
1089
|
+
}
|
|
1034
1090
|
const sig = new SignedXml();
|
|
1035
1091
|
if (!opts.keyFile && !opts.metadata) {
|
|
1036
1092
|
throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS');
|
|
@@ -1061,6 +1117,9 @@ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="{ID}"
|
|
|
1061
1117
|
let checkSafeResult = checkUnsafeSignatureAlgorithm(opts.signatureAlgorithm || '');
|
|
1062
1118
|
hasUnsafeSignatureAlgorithm = checkSafeResult.hasUnsafeSignatureAlgorithm;
|
|
1063
1119
|
unsafeSignatureAlgorithm = checkSafeResult.unsafeSignatureAlgorithm ?? "";
|
|
1120
|
+
if (checkSafeResult.hasUnsafeSignatureAlgorithm && !resolveAllowLegacySha1(opts, here)) {
|
|
1121
|
+
throw new Error('ERR_UNSAFE_SIGNATURE_ALGORITHM');
|
|
1122
|
+
}
|
|
1064
1123
|
sig.signatureAlgorithm = opts.signatureAlgorithm;
|
|
1065
1124
|
sig.loadSignature(signatureNode);
|
|
1066
1125
|
// 验证解密后断言的签名
|
|
@@ -18,7 +18,10 @@ export class IdpMetadata extends Metadata {
|
|
|
18
18
|
constructor(meta) {
|
|
19
19
|
const isFile = isString(meta) || meta instanceof Buffer;
|
|
20
20
|
if (!isFile) {
|
|
21
|
-
const
|
|
21
|
+
const settings = meta;
|
|
22
|
+
const strictSecurity = settings.strictSecurity === true;
|
|
23
|
+
const { entityID, signingCert, encryptCert, nameIDFormat = [], singleSignOnService = [], singleLogoutService = [], artifactResolutionService = [] } = settings;
|
|
24
|
+
const wantAuthnRequestsSigned = settings.wantAuthnRequestsSigned ?? strictSecurity;
|
|
22
25
|
const IDPSSODescriptor = [{
|
|
23
26
|
_attr: {
|
|
24
27
|
WantAuthnRequestsSigned: String(wantAuthnRequestsSigned),
|
package/build/src/metadata-sp.js
CHANGED
|
@@ -26,7 +26,11 @@ export class SpMetadata extends Metadata {
|
|
|
26
26
|
const isFile = isString(meta) || meta instanceof Buffer;
|
|
27
27
|
// use object configuration instead of importing metadata file directly
|
|
28
28
|
if (!isFile) {
|
|
29
|
-
const
|
|
29
|
+
const settings = meta;
|
|
30
|
+
const strictSecurity = settings.strictSecurity === true;
|
|
31
|
+
const { elementsOrder = order.default, entityID, signingCert, encryptCert, wantMessageSigned = false, signatureConfig, nameIDFormat = [], singleLogoutService = [], assertionConsumerService = [], attributeConsumingService = [], artifactResolutionService = [] } = settings;
|
|
32
|
+
const authnRequestsSigned = settings.authnRequestsSigned ?? strictSecurity;
|
|
33
|
+
const wantAssertionsSigned = settings.wantAssertionsSigned ?? strictSecurity;
|
|
30
34
|
const descriptors = {
|
|
31
35
|
KeyDescriptor: [],
|
|
32
36
|
NameIDFormat: [],
|
|
@@ -217,186 +221,153 @@ export class SpMetadata extends Metadata {
|
|
|
217
221
|
* @return {boolean} Wantassertionssigned
|
|
218
222
|
*/
|
|
219
223
|
isWantAssertionsSigned() {
|
|
220
|
-
|
|
224
|
+
const value = this.meta.spSSODescriptor.wantAssertionsSigned;
|
|
225
|
+
if (value === undefined) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return value === 'true';
|
|
221
229
|
}
|
|
222
230
|
/**
|
|
223
231
|
* @desc Get the preference whether it signs request
|
|
224
232
|
* @return {boolean} Authnrequestssigned
|
|
225
233
|
*/
|
|
226
234
|
isAuthnRequestSigned() {
|
|
227
|
-
|
|
235
|
+
const value = this.meta.spSSODescriptor.authnRequestsSigned;
|
|
236
|
+
if (value === undefined) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return value === 'true';
|
|
228
240
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
targetBindingName = resolved;
|
|
241
|
+
getAssertionConsumerService(binding, options = {}) {
|
|
242
|
+
const mode = options.mode ?? 'lenient';
|
|
243
|
+
const normalizeBinding = (value) => String(value ?? '').trim().toLowerCase();
|
|
244
|
+
const normalizeLocation = (value) => String(value ?? '').trim();
|
|
245
|
+
const parseBool = (value) => {
|
|
246
|
+
if (typeof value === 'boolean')
|
|
247
|
+
return value;
|
|
248
|
+
if (typeof value === 'string') {
|
|
249
|
+
const t = value.trim().toLowerCase();
|
|
250
|
+
if (t === 'true')
|
|
251
|
+
return true;
|
|
252
|
+
if (t === 'false')
|
|
253
|
+
return false;
|
|
243
254
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
255
|
+
return null;
|
|
256
|
+
};
|
|
257
|
+
const parseIndex = (value) => {
|
|
258
|
+
if (value === undefined || value === null || String(value).trim() === '')
|
|
259
|
+
return Number.POSITIVE_INFINITY;
|
|
260
|
+
const n = Number.parseInt(String(value), 10);
|
|
261
|
+
return Number.isNaN(n) ? Number.POSITIVE_INFINITY : n;
|
|
262
|
+
};
|
|
263
|
+
const indexHint = parseIndex(options.assertionConsumerServiceIndex);
|
|
264
|
+
const hasIndexHint = Number.isFinite(indexHint);
|
|
265
|
+
const toSafeLocation = (value) => {
|
|
266
|
+
const text = String(value || '').trim();
|
|
267
|
+
// 仅校验协议前缀,保持原始字符串不被规范化改写(兼容历史用例:如 "https:sp.example.org/...")
|
|
268
|
+
if (/^https?:/i.test(text)) {
|
|
269
|
+
return text;
|
|
248
270
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
271
|
+
return '';
|
|
272
|
+
};
|
|
273
|
+
const resolveBindingUri = (input) => {
|
|
274
|
+
if (typeof input !== 'string')
|
|
275
|
+
return null;
|
|
276
|
+
const raw = input.trim();
|
|
277
|
+
if (!raw)
|
|
278
|
+
return null;
|
|
279
|
+
const byKey = namespace.binding[raw];
|
|
280
|
+
if (typeof byKey === 'string')
|
|
281
|
+
return byKey;
|
|
282
|
+
const lower = raw.toLowerCase();
|
|
283
|
+
const candidates = Object.values(namespace.binding).filter((v) => typeof v === 'string');
|
|
284
|
+
const byUri = candidates.find((v) => v.toLowerCase() === lower);
|
|
285
|
+
if (byUri)
|
|
286
|
+
return byUri;
|
|
287
|
+
const aliasMap = {
|
|
288
|
+
post: namespace.binding.post,
|
|
289
|
+
redirect: namespace.binding.redirect,
|
|
290
|
+
artifact: namespace.binding.artifact,
|
|
291
|
+
simplesign: namespace.binding.simpleSign,
|
|
292
|
+
'simple-sign': namespace.binding.simpleSign,
|
|
293
|
+
soap: namespace.binding.soap,
|
|
294
|
+
};
|
|
295
|
+
return aliasMap[lower] || null;
|
|
296
|
+
};
|
|
297
|
+
const sortCandidates = (list) => [...list].sort((a, b) => {
|
|
298
|
+
const score = (item) => item.isDefault === true ? 0 : (item.isDefault === null ? 1 : 2);
|
|
299
|
+
const sA = score(a);
|
|
300
|
+
const sB = score(b);
|
|
301
|
+
if (sA !== sB)
|
|
302
|
+
return sA - sB;
|
|
303
|
+
if (a.index !== b.index)
|
|
304
|
+
return a.index - b.index;
|
|
305
|
+
return a.order - b.order;
|
|
306
|
+
});
|
|
307
|
+
const pickBest = (list) => {
|
|
278
308
|
if (list.length === 0)
|
|
279
|
-
return
|
|
280
|
-
|
|
281
|
-
return list[0];
|
|
282
|
-
const tier1 = []; // isDefault === 'true'
|
|
283
|
-
const tier2 = []; // isDefault 不存在 或 不为 'false'
|
|
284
|
-
const tier3 = []; // 明确 isDefault === 'false'
|
|
285
|
-
list.forEach((item, originalIndex) => {
|
|
286
|
-
const isDef = item.isDefault;
|
|
287
|
-
const isTrue = (typeof isDef === 'string' && isDef.toLowerCase() === 'true');
|
|
288
|
-
const isFalse = (typeof isDef === 'string' && isDef.toLowerCase() === 'false');
|
|
289
|
-
if (isTrue) {
|
|
290
|
-
tier1.push(item);
|
|
291
|
-
}
|
|
292
|
-
else if (!isFalse) {
|
|
293
|
-
tier2.push(item);
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
tier3.push(item);
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
let selectedTier = [];
|
|
300
|
-
if (tier1.length > 0) {
|
|
301
|
-
selectedTier = tier1;
|
|
302
|
-
}
|
|
303
|
-
else if (tier2.length > 0) {
|
|
304
|
-
selectedTier = tier2;
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
selectedTier = tier3;
|
|
308
|
-
}
|
|
309
|
-
// 二级排序:Index 升序 -> 原始顺序
|
|
310
|
-
const indexedTier = selectedTier.map((item, idx) => ({ item, originalIdx: idx }));
|
|
311
|
-
indexedTier.sort((a, b) => {
|
|
312
|
-
// 安全解析 index
|
|
313
|
-
let idxA = Infinity;
|
|
314
|
-
let idxB = Infinity;
|
|
315
|
-
if (a.item.index !== undefined && a.item.index !== null) {
|
|
316
|
-
const parsed = parseInt(a.item.index, 10);
|
|
317
|
-
if (!isNaN(parsed))
|
|
318
|
-
idxA = parsed;
|
|
319
|
-
}
|
|
320
|
-
if (b.item.index !== undefined && b.item.index !== null) {
|
|
321
|
-
const parsed = parseInt(b.item.index, 10);
|
|
322
|
-
if (!isNaN(parsed))
|
|
323
|
-
idxB = parsed;
|
|
324
|
-
}
|
|
325
|
-
if (idxA !== idxB) {
|
|
326
|
-
return idxA - idxB;
|
|
327
|
-
}
|
|
328
|
-
return a.originalIdx - b.originalIdx;
|
|
329
|
-
});
|
|
330
|
-
return indexedTier[0].item;
|
|
309
|
+
return null;
|
|
310
|
+
return sortCandidates(list)[0] ?? null;
|
|
331
311
|
};
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
312
|
+
const pickByIndex = (list) => {
|
|
313
|
+
if (!hasIndexHint)
|
|
314
|
+
return null;
|
|
315
|
+
const hit = list.filter((item) => item.index === indexHint);
|
|
316
|
+
return pickBest(hit);
|
|
317
|
+
};
|
|
318
|
+
const acsData = this.meta.assertionConsumerService;
|
|
319
|
+
const rawCandidates = Array.isArray(acsData) ? acsData : (acsData && typeof acsData === 'object' ? [acsData] : []);
|
|
320
|
+
if (rawCandidates.length === 0)
|
|
321
|
+
return '';
|
|
322
|
+
const normalizedCandidates = rawCandidates
|
|
323
|
+
.map((item, order) => ({
|
|
324
|
+
binding: String(item?.binding ?? item?.Binding ?? '').trim(),
|
|
325
|
+
location: toSafeLocation(normalizeLocation(item?.location ?? item?.Location ?? item?.responseLocation ?? item?.ResponseLocation ?? '')),
|
|
326
|
+
isDefault: parseBool(item?.isDefault ?? item?.IsDefault ?? item?.default),
|
|
327
|
+
index: parseIndex(item?.index ?? item?.Index),
|
|
328
|
+
order,
|
|
329
|
+
}))
|
|
330
|
+
.filter((item) => item.location);
|
|
331
|
+
if (normalizedCandidates.length === 0)
|
|
332
|
+
return '';
|
|
333
|
+
const dedupMap = new Map();
|
|
334
|
+
for (const item of normalizedCandidates) {
|
|
335
|
+
const key = `${normalizeBinding(item.binding)}|${item.location}|${Number.isFinite(item.index) ? item.index : 'NA'}`;
|
|
336
|
+
if (!dedupMap.has(key))
|
|
337
|
+
dedupMap.set(key, item);
|
|
347
338
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
//console.log(`[ACS Selection] ⚠️ 指定 Binding 未找到可用项,进入降级策略...`);
|
|
358
|
-
}
|
|
359
|
-
// 2.1 全局寻找 isDefault='true'
|
|
360
|
-
const globalDefaults = allCandidates.filter(obj => obj.isDefault && String(obj.isDefault).toLowerCase() === 'true');
|
|
361
|
-
if (globalDefaults.length > 0) {
|
|
362
|
-
const best = selectBestFromList(globalDefaults);
|
|
363
|
-
if (best) {
|
|
364
|
-
//console.log(`[ACS Selection] ✅ (阶段2) 跨 Binding 找到 isDefault=true: ${best.location} (${best.binding})`);
|
|
365
|
-
resultLocation = best.location;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
// 2.2 全局寻找 isDefault 不为 'false' (隐式默认)
|
|
369
|
-
if (!resultLocation) {
|
|
370
|
-
const globalNonFalse = allCandidates.filter(obj => {
|
|
371
|
-
const isDef = obj.isDefault;
|
|
372
|
-
const isFalse = (typeof isDef === 'string' && isDef.toLowerCase() === 'false');
|
|
373
|
-
return !isFalse;
|
|
374
|
-
});
|
|
375
|
-
if (globalNonFalse.length > 0) {
|
|
376
|
-
const best = selectBestFromList(globalNonFalse);
|
|
377
|
-
if (best) {
|
|
378
|
-
//console.log(`[ACS Selection] ✅ (阶段3) 跨 Binding 找到隐式默认: ${best.location} (${best.binding})`);
|
|
379
|
-
resultLocation = best.location;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
// 2.3 终极兜底:所有数据中 index 最小
|
|
384
|
-
if (!resultLocation) {
|
|
385
|
-
const best = selectBestFromList(allCandidates);
|
|
386
|
-
if (best) {
|
|
387
|
-
// console.log(`[ACS Selection] ✅ (阶段4) 终极兜底,选择 index 最小: ${best.location} (${best.binding})`);
|
|
388
|
-
resultLocation = best.location;
|
|
389
|
-
}
|
|
339
|
+
const allCandidates = Array.from(dedupMap.values());
|
|
340
|
+
const targetBindingName = resolveBindingUri(binding);
|
|
341
|
+
const boundCandidates = targetBindingName
|
|
342
|
+
? allCandidates.filter((item) => normalizeBinding(item.binding) === normalizeBinding(targetBindingName))
|
|
343
|
+
: [];
|
|
344
|
+
const warnDefaultConflict = (list, label) => {
|
|
345
|
+
const defaults = list.filter((item) => item.isDefault === true);
|
|
346
|
+
if (defaults.length > 1) {
|
|
347
|
+
console.warn(`[ACS Selection] Multiple isDefault=true found in ${label}; fallback to index/order.`);
|
|
390
348
|
}
|
|
349
|
+
};
|
|
350
|
+
warnDefaultConflict(targetBindingName ? boundCandidates : allCandidates, targetBindingName ? `binding=${targetBindingName}` : 'all bindings');
|
|
351
|
+
// 1) index 优先:先同 binding,再全局(仅 lenient)
|
|
352
|
+
const indexedInBinding = pickByIndex(boundCandidates);
|
|
353
|
+
if (indexedInBinding)
|
|
354
|
+
return indexedInBinding.location;
|
|
355
|
+
if (targetBindingName && mode === 'strict' && boundCandidates.length === 0) {
|
|
356
|
+
return '';
|
|
391
357
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const errorMsg = "SAML Configuration Error: Unable to select any valid AssertionConsumerService URL. Metadata might be empty or malformed.";
|
|
397
|
-
console.error(`[ACS Selection] ❌ ${errorMsg}`);
|
|
398
|
-
throw new Error(errorMsg);
|
|
358
|
+
if (mode === 'lenient') {
|
|
359
|
+
const indexedGlobal = pickByIndex(allCandidates);
|
|
360
|
+
if (indexedGlobal)
|
|
361
|
+
return indexedGlobal.location;
|
|
399
362
|
}
|
|
400
|
-
|
|
363
|
+
// 2) 绑定内最优
|
|
364
|
+
const bestBound = pickBest(boundCandidates);
|
|
365
|
+
if (bestBound)
|
|
366
|
+
return bestBound.location;
|
|
367
|
+
if (mode === 'strict' && targetBindingName)
|
|
368
|
+
return '';
|
|
369
|
+
// 3) 全局回退
|
|
370
|
+
const bestGlobal = pickBest(allCandidates);
|
|
371
|
+
return bestGlobal?.location ?? '';
|
|
401
372
|
}
|
|
402
373
|
}
|