samlesa 4.3.2 → 4.3.4
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/metadata-sp.js +160 -166
- package/package.json +1 -1
- package/types/src/metadata-sp.d.ts +8 -1
- package/types/src/metadata-sp.d.ts.map +1 -1
package/build/src/metadata-sp.js
CHANGED
|
@@ -8,6 +8,37 @@ import { namespace, elementsOrder as order } from './urn.js';
|
|
|
8
8
|
import libsaml from './libsaml.js';
|
|
9
9
|
import { castArrayOpt, isNonEmptyArray, isString } from './utility.js';
|
|
10
10
|
import xml from 'xml';
|
|
11
|
+
import { select } from 'xpath';
|
|
12
|
+
import { getContext } from './api.js';
|
|
13
|
+
function toNodeArray(result) {
|
|
14
|
+
if (Array.isArray(result))
|
|
15
|
+
return result;
|
|
16
|
+
if (result)
|
|
17
|
+
return [result];
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
function unwrapSingleEntityDescriptorMetadata(meta) {
|
|
21
|
+
const metadataText = meta.toString();
|
|
22
|
+
if (!/<(?:\w+:)?EntitiesDescriptor\b/i.test(metadataText)) {
|
|
23
|
+
return meta;
|
|
24
|
+
}
|
|
25
|
+
const { dom } = getContext();
|
|
26
|
+
const rootDoc = dom.parseFromString(metadataText, 'application/xml');
|
|
27
|
+
const parserErrors = rootDoc.getElementsByTagName('parsererror');
|
|
28
|
+
if (parserErrors.length > 0) {
|
|
29
|
+
return meta;
|
|
30
|
+
}
|
|
31
|
+
const entityNodes = toNodeArray(
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
select("/*[local-name(.)='EntitiesDescriptor']/*[local-name(.)='EntityDescriptor']", rootDoc));
|
|
34
|
+
if (entityNodes.length === 0) {
|
|
35
|
+
return meta;
|
|
36
|
+
}
|
|
37
|
+
if (entityNodes.length > 1) {
|
|
38
|
+
throw new Error('ERR_MULTIPLE_METADATA_ENTITYDESCRIPTOR');
|
|
39
|
+
}
|
|
40
|
+
return entityNodes[0].toString();
|
|
41
|
+
}
|
|
11
42
|
/*
|
|
12
43
|
* @desc interface function
|
|
13
44
|
*/
|
|
@@ -24,6 +55,10 @@ export class SpMetadata extends Metadata {
|
|
|
24
55
|
*/
|
|
25
56
|
constructor(meta) {
|
|
26
57
|
const isFile = isString(meta) || meta instanceof Buffer;
|
|
58
|
+
let normalizedMeta = meta;
|
|
59
|
+
if (isFile) {
|
|
60
|
+
normalizedMeta = unwrapSingleEntityDescriptorMetadata(meta);
|
|
61
|
+
}
|
|
27
62
|
// use object configuration instead of importing metadata file directly
|
|
28
63
|
if (!isFile) {
|
|
29
64
|
const settings = meta;
|
|
@@ -186,7 +221,7 @@ export class SpMetadata extends Metadata {
|
|
|
186
221
|
descriptors[name].forEach(e => SPSSODescriptor.push({ [name]: e }));
|
|
187
222
|
});
|
|
188
223
|
// Re-assign the meta reference as a XML string|Buffer for use with the parent constructor
|
|
189
|
-
|
|
224
|
+
normalizedMeta = xml([{
|
|
190
225
|
EntityDescriptor: [{
|
|
191
226
|
_attr: {
|
|
192
227
|
entityID,
|
|
@@ -198,7 +233,7 @@ export class SpMetadata extends Metadata {
|
|
|
198
233
|
}]);
|
|
199
234
|
}
|
|
200
235
|
// Use the re-assigned meta object reference here
|
|
201
|
-
super(
|
|
236
|
+
super(normalizedMeta, [
|
|
202
237
|
{
|
|
203
238
|
key: 'spSSODescriptor',
|
|
204
239
|
localPath: ['EntityDescriptor', 'SPSSODescriptor'],
|
|
@@ -238,177 +273,136 @@ export class SpMetadata extends Metadata {
|
|
|
238
273
|
}
|
|
239
274
|
return value === 'true';
|
|
240
275
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
targetBindingName = resolved;
|
|
276
|
+
getAssertionConsumerService(binding, options = {}) {
|
|
277
|
+
const mode = options.mode ?? 'lenient';
|
|
278
|
+
const normalizeBinding = (value) => String(value ?? '').trim().toLowerCase();
|
|
279
|
+
const normalizeLocation = (value) => String(value ?? '').trim();
|
|
280
|
+
const parseBool = (value) => {
|
|
281
|
+
if (typeof value === 'boolean')
|
|
282
|
+
return value;
|
|
283
|
+
if (typeof value === 'string') {
|
|
284
|
+
const t = value.trim().toLowerCase();
|
|
285
|
+
if (t === 'true')
|
|
286
|
+
return true;
|
|
287
|
+
if (t === 'false')
|
|
288
|
+
return false;
|
|
255
289
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
290
|
+
return null;
|
|
291
|
+
};
|
|
292
|
+
const parseIndex = (value) => {
|
|
293
|
+
if (value === undefined || value === null || String(value).trim() === '')
|
|
294
|
+
return Number.POSITIVE_INFINITY;
|
|
295
|
+
const n = Number.parseInt(String(value), 10);
|
|
296
|
+
return Number.isNaN(n) ? Number.POSITIVE_INFINITY : n;
|
|
297
|
+
};
|
|
298
|
+
const indexHint = parseIndex(options.assertionConsumerServiceIndex);
|
|
299
|
+
const hasIndexHint = Number.isFinite(indexHint);
|
|
300
|
+
const toSafeLocation = (value) => {
|
|
301
|
+
const text = String(value || '').trim();
|
|
302
|
+
// 仅校验协议前缀,保持原始字符串不被规范化改写(兼容历史用例:如 "https:sp.example.org/...")
|
|
303
|
+
if (/^https?:/i.test(text)) {
|
|
304
|
+
return text;
|
|
260
305
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
306
|
+
return '';
|
|
307
|
+
};
|
|
308
|
+
const resolveBindingUri = (input) => {
|
|
309
|
+
if (typeof input !== 'string')
|
|
310
|
+
return null;
|
|
311
|
+
const raw = input.trim();
|
|
312
|
+
if (!raw)
|
|
313
|
+
return null;
|
|
314
|
+
const byKey = namespace.binding[raw];
|
|
315
|
+
if (typeof byKey === 'string')
|
|
316
|
+
return byKey;
|
|
317
|
+
const lower = raw.toLowerCase();
|
|
318
|
+
const candidates = Object.values(namespace.binding).filter((v) => typeof v === 'string');
|
|
319
|
+
const byUri = candidates.find((v) => v.toLowerCase() === lower);
|
|
320
|
+
if (byUri)
|
|
321
|
+
return byUri;
|
|
322
|
+
const aliasMap = {
|
|
323
|
+
post: namespace.binding.post,
|
|
324
|
+
redirect: namespace.binding.redirect,
|
|
325
|
+
artifact: namespace.binding.artifact,
|
|
326
|
+
simplesign: namespace.binding.simpleSign,
|
|
327
|
+
'simple-sign': namespace.binding.simpleSign,
|
|
328
|
+
soap: namespace.binding.soap,
|
|
329
|
+
};
|
|
330
|
+
return aliasMap[lower] || null;
|
|
331
|
+
};
|
|
332
|
+
const sortCandidates = (list) => [...list].sort((a, b) => {
|
|
333
|
+
const score = (item) => item.isDefault === true ? 0 : (item.isDefault === null ? 1 : 2);
|
|
334
|
+
const sA = score(a);
|
|
335
|
+
const sB = score(b);
|
|
336
|
+
if (sA !== sB)
|
|
337
|
+
return sA - sB;
|
|
338
|
+
if (a.index !== b.index)
|
|
339
|
+
return a.index - b.index;
|
|
340
|
+
return a.order - b.order;
|
|
341
|
+
});
|
|
342
|
+
const pickBest = (list) => {
|
|
290
343
|
if (list.length === 0)
|
|
291
|
-
return
|
|
292
|
-
|
|
293
|
-
return list[0];
|
|
294
|
-
const tier1 = []; // isDefault === 'true'
|
|
295
|
-
const tier2 = []; // isDefault 不存在 或 不为 'false'
|
|
296
|
-
const tier3 = []; // 明确 isDefault === 'false'
|
|
297
|
-
list.forEach((item, originalIndex) => {
|
|
298
|
-
const isDef = item.isDefault;
|
|
299
|
-
const isTrue = (typeof isDef === 'string' && isDef.toLowerCase() === 'true');
|
|
300
|
-
const isFalse = (typeof isDef === 'string' && isDef.toLowerCase() === 'false');
|
|
301
|
-
if (isTrue) {
|
|
302
|
-
tier1.push(item);
|
|
303
|
-
}
|
|
304
|
-
else if (!isFalse) {
|
|
305
|
-
tier2.push(item);
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
tier3.push(item);
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
let selectedTier = [];
|
|
312
|
-
if (tier1.length > 0) {
|
|
313
|
-
selectedTier = tier1;
|
|
314
|
-
}
|
|
315
|
-
else if (tier2.length > 0) {
|
|
316
|
-
selectedTier = tier2;
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
selectedTier = tier3;
|
|
320
|
-
}
|
|
321
|
-
// 二级排序:Index 升序 -> 原始顺序
|
|
322
|
-
const indexedTier = selectedTier.map((item, idx) => ({ item, originalIdx: idx }));
|
|
323
|
-
indexedTier.sort((a, b) => {
|
|
324
|
-
// 安全解析 index
|
|
325
|
-
let idxA = Infinity;
|
|
326
|
-
let idxB = Infinity;
|
|
327
|
-
if (a.item.index !== undefined && a.item.index !== null) {
|
|
328
|
-
const parsed = parseInt(a.item.index, 10);
|
|
329
|
-
if (!isNaN(parsed))
|
|
330
|
-
idxA = parsed;
|
|
331
|
-
}
|
|
332
|
-
if (b.item.index !== undefined && b.item.index !== null) {
|
|
333
|
-
const parsed = parseInt(b.item.index, 10);
|
|
334
|
-
if (!isNaN(parsed))
|
|
335
|
-
idxB = parsed;
|
|
336
|
-
}
|
|
337
|
-
if (idxA !== idxB) {
|
|
338
|
-
return idxA - idxB;
|
|
339
|
-
}
|
|
340
|
-
return a.originalIdx - b.originalIdx;
|
|
341
|
-
});
|
|
342
|
-
return indexedTier[0].item;
|
|
344
|
+
return null;
|
|
345
|
+
return sortCandidates(list)[0] ?? null;
|
|
343
346
|
};
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
347
|
+
const pickByIndex = (list) => {
|
|
348
|
+
if (!hasIndexHint)
|
|
349
|
+
return null;
|
|
350
|
+
const hit = list.filter((item) => item.index === indexHint);
|
|
351
|
+
return pickBest(hit);
|
|
352
|
+
};
|
|
353
|
+
const acsData = this.meta.assertionConsumerService;
|
|
354
|
+
const rawCandidates = Array.isArray(acsData) ? acsData : (acsData && typeof acsData === 'object' ? [acsData] : []);
|
|
355
|
+
if (rawCandidates.length === 0)
|
|
356
|
+
return '';
|
|
357
|
+
const normalizedCandidates = rawCandidates
|
|
358
|
+
.map((item, order) => ({
|
|
359
|
+
binding: String(item?.binding ?? item?.Binding ?? '').trim(),
|
|
360
|
+
location: toSafeLocation(normalizeLocation(item?.location ?? item?.Location ?? item?.responseLocation ?? item?.ResponseLocation ?? '')),
|
|
361
|
+
isDefault: parseBool(item?.isDefault ?? item?.IsDefault ?? item?.default),
|
|
362
|
+
index: parseIndex(item?.index ?? item?.Index),
|
|
363
|
+
order,
|
|
364
|
+
}))
|
|
365
|
+
.filter((item) => item.location);
|
|
366
|
+
if (normalizedCandidates.length === 0)
|
|
367
|
+
return '';
|
|
368
|
+
const dedupMap = new Map();
|
|
369
|
+
for (const item of normalizedCandidates) {
|
|
370
|
+
const key = `${normalizeBinding(item.binding)}|${item.location}|${Number.isFinite(item.index) ? item.index : 'NA'}`;
|
|
371
|
+
if (!dedupMap.has(key))
|
|
372
|
+
dedupMap.set(key, item);
|
|
359
373
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
//console.log(`[ACS Selection] ⚠️ 指定 Binding 未找到可用项,进入降级策略...`);
|
|
370
|
-
}
|
|
371
|
-
// 2.1 全局寻找 isDefault='true'
|
|
372
|
-
const globalDefaults = allCandidates.filter(obj => obj.isDefault && String(obj.isDefault).toLowerCase() === 'true');
|
|
373
|
-
if (globalDefaults.length > 0) {
|
|
374
|
-
const best = selectBestFromList(globalDefaults);
|
|
375
|
-
if (best) {
|
|
376
|
-
//console.log(`[ACS Selection] ✅ (阶段2) 跨 Binding 找到 isDefault=true: ${best.location} (${best.binding})`);
|
|
377
|
-
resultLocation = best.location;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
// 2.2 全局寻找 isDefault 不为 'false' (隐式默认)
|
|
381
|
-
if (!resultLocation) {
|
|
382
|
-
const globalNonFalse = allCandidates.filter(obj => {
|
|
383
|
-
const isDef = obj.isDefault;
|
|
384
|
-
const isFalse = (typeof isDef === 'string' && isDef.toLowerCase() === 'false');
|
|
385
|
-
return !isFalse;
|
|
386
|
-
});
|
|
387
|
-
if (globalNonFalse.length > 0) {
|
|
388
|
-
const best = selectBestFromList(globalNonFalse);
|
|
389
|
-
if (best) {
|
|
390
|
-
//console.log(`[ACS Selection] ✅ (阶段3) 跨 Binding 找到隐式默认: ${best.location} (${best.binding})`);
|
|
391
|
-
resultLocation = best.location;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
// 2.3 终极兜底:所有数据中 index 最小
|
|
396
|
-
if (!resultLocation) {
|
|
397
|
-
const best = selectBestFromList(allCandidates);
|
|
398
|
-
if (best) {
|
|
399
|
-
// console.log(`[ACS Selection] ✅ (阶段4) 终极兜底,选择 index 最小: ${best.location} (${best.binding})`);
|
|
400
|
-
resultLocation = best.location;
|
|
401
|
-
}
|
|
374
|
+
const allCandidates = Array.from(dedupMap.values());
|
|
375
|
+
const targetBindingName = resolveBindingUri(binding);
|
|
376
|
+
const boundCandidates = targetBindingName
|
|
377
|
+
? allCandidates.filter((item) => normalizeBinding(item.binding) === normalizeBinding(targetBindingName))
|
|
378
|
+
: [];
|
|
379
|
+
const warnDefaultConflict = (list, label) => {
|
|
380
|
+
const defaults = list.filter((item) => item.isDefault === true);
|
|
381
|
+
if (defaults.length > 1) {
|
|
382
|
+
console.warn(`[ACS Selection] Multiple isDefault=true found in ${label}; fallback to index/order.`);
|
|
402
383
|
}
|
|
384
|
+
};
|
|
385
|
+
warnDefaultConflict(targetBindingName ? boundCandidates : allCandidates, targetBindingName ? `binding=${targetBindingName}` : 'all bindings');
|
|
386
|
+
// 1) index 优先:先同 binding,再全局(仅 lenient)
|
|
387
|
+
const indexedInBinding = pickByIndex(boundCandidates);
|
|
388
|
+
if (indexedInBinding)
|
|
389
|
+
return indexedInBinding.location;
|
|
390
|
+
if (targetBindingName && mode === 'strict' && boundCandidates.length === 0) {
|
|
391
|
+
return '';
|
|
403
392
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const errorMsg = "SAML Configuration Error: Unable to select any valid AssertionConsumerService URL. Metadata might be empty or malformed.";
|
|
409
|
-
console.error(`[ACS Selection] ❌ ${errorMsg}`);
|
|
410
|
-
throw new Error(errorMsg);
|
|
393
|
+
if (mode === 'lenient') {
|
|
394
|
+
const indexedGlobal = pickByIndex(allCandidates);
|
|
395
|
+
if (indexedGlobal)
|
|
396
|
+
return indexedGlobal.location;
|
|
411
397
|
}
|
|
412
|
-
|
|
398
|
+
// 2) 绑定内最优
|
|
399
|
+
const bestBound = pickBest(boundCandidates);
|
|
400
|
+
if (bestBound)
|
|
401
|
+
return bestBound.location;
|
|
402
|
+
if (mode === 'strict' && targetBindingName)
|
|
403
|
+
return '';
|
|
404
|
+
// 3) 全局回退
|
|
405
|
+
const bestGlobal = pickBest(allCandidates);
|
|
406
|
+
return bestGlobal?.location ?? '';
|
|
413
407
|
}
|
|
414
408
|
}
|
package/package.json
CHANGED
|
@@ -7,6 +7,11 @@ import Metadata, { type MetadataInterface } from './metadata.js';
|
|
|
7
7
|
import type { MetadataSpConstructor } from './types.js';
|
|
8
8
|
export interface SpMetadataInterface extends MetadataInterface {
|
|
9
9
|
}
|
|
10
|
+
type AcsSelectionMode = 'strict' | 'lenient';
|
|
11
|
+
interface AcsSelectionOptions {
|
|
12
|
+
mode?: AcsSelectionMode;
|
|
13
|
+
assertionConsumerServiceIndex?: string | number | null;
|
|
14
|
+
}
|
|
10
15
|
export default function (meta: MetadataSpConstructor): SpMetadata;
|
|
11
16
|
/**
|
|
12
17
|
* @desc SP Metadata is for creating Service Provider, provides a set of API to manage the actions in SP.
|
|
@@ -32,6 +37,8 @@ export declare class SpMetadata extends Metadata {
|
|
|
32
37
|
* @param {string} binding protocol binding (e.g. redirect, post)
|
|
33
38
|
* @return {string/[string]} URL of endpoint(s)
|
|
34
39
|
*/
|
|
35
|
-
getAssertionConsumerService(binding
|
|
40
|
+
getAssertionConsumerService(binding?: string): string;
|
|
41
|
+
getAssertionConsumerService(binding: string | undefined, options: AcsSelectionOptions): string;
|
|
36
42
|
}
|
|
43
|
+
export {};
|
|
37
44
|
//# sourceMappingURL=metadata-sp.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata-sp.d.ts","sourceRoot":"","sources":["../../src/metadata-sp.ts"],"names":[],"mappings":"AAAA;;;;EAIE;AACF,OAAO,QAAQ,EAAE,EAAC,KAAK,iBAAiB,EAAC,MAAM,eAAe,CAAC;AAE/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"metadata-sp.d.ts","sourceRoot":"","sources":["../../src/metadata-sp.ts"],"names":[],"mappings":"AAAA;;;;EAIE;AACF,OAAO,QAAQ,EAAE,EAAC,KAAK,iBAAiB,EAAC,MAAM,eAAe,CAAC;AAE/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAQxD,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;CAE7D;AAYD,KAAK,gBAAgB,GAAG,QAAQ,GAAG,SAAS,CAAC;AAC7C,UAAU,mBAAmB;IAC3B,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,6BAA6B,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CACxD;AAqCD,MAAM,CAAC,OAAO,WAAU,IAAI,EAAE,qBAAqB,cAElD;AAED;;EAEE;AACF,qBAAa,UAAW,SAAQ,QAAQ;IAEtC;;;MAGE;gBACU,IAAI,EAAE,qBAAqB;IAmOvC;;;MAGE;IACK,sBAAsB,IAAI,OAAO;IAOxC;;;MAGE;IACK,oBAAoB,IAAI,OAAO;IAOtC;;;;MAIE;IACK,2BAA2B,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM;IACrD,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,mBAAmB,GAAG,MAAM;CAmItG"}
|