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.
@@ -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).toString()));
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).toString();
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
@@ -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
- if (this.entityMeta.isAuthnRequestSigned() !== idp.entityMeta.isWantAuthnRequestsSigned()) {
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;
@@ -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');
@@ -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
- if (sigAlg) {
171
- const algAlias = cryptoAlgorithmMapping[sigAlg];
172
- if (algAlias !== undefined) {
173
- return algAlias;
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
- return cryptoAlgorithmMapping[signatureAlgorithms.RSA_SHA1];
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 ?? 'sha1', //default sha256
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 { entityID, signingCert, encryptCert, wantAuthnRequestsSigned = false, nameIDFormat = [], singleSignOnService = [], singleLogoutService = [], artifactResolutionService = [] } = meta;
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),
@@ -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 { elementsOrder = order.default, entityID, signingCert, encryptCert, authnRequestsSigned = false, wantAssertionsSigned = false, wantMessageSigned = false, signatureConfig, nameIDFormat = [], singleLogoutService = [], assertionConsumerService = [], attributeConsumingService = [], artifactResolutionService = [] } = meta;
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
- return this.meta.spSSODescriptor.wantAssertionsSigned === 'true';
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
- return this.meta.spSSODescriptor.authnRequestsSigned === 'true';
235
+ const value = this.meta.spSSODescriptor.authnRequestsSigned;
236
+ if (value === undefined) {
237
+ return false;
238
+ }
239
+ return value === 'true';
228
240
  }
229
- /**
230
- * @desc Get the entity endpoint for assertion consumer service
231
- * @param {string} binding protocol binding (e.g. redirect, post)
232
- * @return {string/[string]} URL of endpoint(s)
233
- */
234
- getAssertionConsumerService(binding) {
235
- // 1. 预处理 binding 参数
236
- // 如果 binding 不是字符串,视为“未指定 Binding 偏好”,直接进入全局兜底逻辑 (Fallback Mode)
237
- let targetBindingName = null;
238
- let useFallbackMode = false;
239
- if (typeof binding === 'string') {
240
- const resolved = namespace.binding[binding];
241
- if (resolved) {
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
- else {
245
- // 字符串传入了但 namespace 里没找到,也视为无效,进入兜底
246
- //console.warn(`[ACS Selection] 未知的 binding 键值: ${binding}, 将启用全局兜底策略.`);
247
- useFallbackMode = true;
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
- else {
251
- // 非字符串类型 (null, undefined, object 等),直接启用兜底
252
- // console.warn(`[ACS Selection] binding 参数类型错误 (${typeof binding}), 将启用全局兜底策略.`);
253
- useFallbackMode = true;
254
- }
255
- const acsData = this.meta.assertionConsumerService;
256
- // 2. 标准化数据为数组
257
- let allCandidates = [];
258
- if (Array.isArray(acsData)) {
259
- allCandidates = acsData;
260
- }
261
- else if (acsData && typeof acsData === 'object') {
262
- allCandidates = [acsData];
263
- }
264
- else {
265
- // 数据源本身为空,直接报错
266
- return "";
267
- /* throw new Error("SAML Metadata Error: No AssertionConsumerService definitions found in metadata.");*/
268
- }
269
- if (allCandidates.length === 0) {
270
- return "";
271
- /* throw new Error("SAML Metadata Error: AssertionConsumerService list is empty.");*/
272
- }
273
- /**
274
- * 核心排序与选择函数
275
- * 输入一个候选数组,返回最佳的一个对象
276
- */
277
- const selectBestFromList = (list) => {
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 undefined;
280
- if (list.length === 1)
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
- let resultLocation = undefined;
333
- // ==========================================
334
- // 第一阶段:尝试在指定的 binding 中查找 (仅在非兜底模式下执行)
335
- // ==========================================
336
- if (!useFallbackMode && targetBindingName) {
337
- const matchedByBinding = allCandidates.filter(obj => obj.binding === targetBindingName);
338
- // console.log(`[ACS Selection] 目标 Binding: ${targetBindingName}`);
339
- //console.log(`[ACS Selection] 匹配该 Binding 的数量: ${matchedByBinding.length}`);
340
- if (matchedByBinding.length > 0) {
341
- const bestInBinding = selectBestFromList(matchedByBinding);
342
- if (bestInBinding) {
343
- // console.log(`[ACS Selection] (阶段1) 在指定 Binding 中找到最佳项: ${bestInBinding.location}`);
344
- resultLocation = bestInBinding.location;
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
- // 如果第一阶段没找到,或者一开始就进入了兜底模式 (useFallbackMode)
351
- // ==========================================
352
- if (!resultLocation) {
353
- if (useFallbackMode) {
354
- //console.log(`[ACS Selection] ⚠️ 启用全局兜底策略 (未指定有效 Binding)...`);
355
- }
356
- else {
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
- if (!resultLocation) {
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
- return resultLocation;
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
  }