samlify
Version:
Node.js library for SAML SSO
336 lines • 12.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.logoutResponseFields = exports.logoutRequestFields = exports.loginResponseFields = exports.logoutResponseStatusFields = exports.loginResponseStatusFields = exports.loginRequestFields = void 0;
exports.extract = extract;
/**
* @file extractor.ts
* @author tngan
* @desc Declarative XPath extractor for SAML messages. Defines the field
* catalogs (login/logout request & response) and a generic `extract` that
* evaluates them against an XML document.
*/
var xpath_1 = require("xpath");
var utility_1 = require("./utility");
var api_1 = require("./api");
/**
* Coerce the heterogeneous return type of `xpath.select` into a Node array.
*/
function toNodeArray(result) {
if (Array.isArray(result))
return result;
if (result != null && typeof result === 'object' && 'nodeType' in result) {
return [result];
}
return [];
}
/**
* Build an absolute XPath expression from a list of local-name segments.
* A segment prefixed with `~` matches any element whose local-name contains
* the remaining text (case-sensitive substring).
*/
function buildAbsoluteXPath(paths) {
return paths.reduce(function (currentPath, name) {
var isWildcard = name.startsWith('~');
if (isWildcard) {
var pathName = name.replace('~', '');
return currentPath + "/*[contains(local-name(), ".concat((0, utility_1.escapeXPathValue)(pathName), ")]");
}
return currentPath + "/*[local-name(.)=".concat((0, utility_1.escapeXPathValue)(name), "]");
}, '');
}
/**
* Append an attribute selector to an XPath. Zero attributes select text
* content; one attribute selects that attribute; multiple attributes use an
* `or` filter.
*/
function buildAttributeXPath(attributes) {
if (attributes.length === 0) {
return '/text()';
}
if (attributes.length === 1) {
return "/@".concat(attributes[0]);
}
var filters = attributes.map(function (attribute) { return "name()=".concat((0, utility_1.escapeXPathValue)(attribute)); }).join(' or ');
return "/@*[".concat(filters, "]");
}
/** Default extractor fields for an inbound `AuthnRequest` (login request). */
exports.loginRequestFields = [
{
key: 'request',
localPath: ['AuthnRequest'],
attributes: ['ID', 'IssueInstant', 'Destination', 'AssertionConsumerServiceURL'],
},
{
key: 'issuer',
localPath: ['AuthnRequest', 'Issuer'],
attributes: [],
},
{
key: 'nameIDPolicy',
localPath: ['AuthnRequest', 'NameIDPolicy'],
attributes: ['Format', 'AllowCreate'],
},
{
key: 'authnContextClassRef',
localPath: ['AuthnRequest', 'AuthnContextClassRef'],
attributes: [],
},
{
key: 'signature',
localPath: ['AuthnRequest', 'Signature'],
attributes: [],
context: true,
},
];
/** Two-tier status code extractor for login responses. */
exports.loginResponseStatusFields = [
{
key: 'top',
localPath: ['Response', 'Status', 'StatusCode'],
attributes: ['Value'],
},
{
key: 'second',
localPath: ['Response', 'Status', 'StatusCode', 'StatusCode'],
attributes: ['Value'],
},
];
/** Two-tier status code extractor for logout responses. */
exports.logoutResponseStatusFields = [
{
key: 'top',
localPath: ['LogoutResponse', 'Status', 'StatusCode'],
attributes: ['Value'],
},
{
key: 'second',
localPath: ['LogoutResponse', 'Status', 'StatusCode', 'StatusCode'],
attributes: ['Value'],
},
];
/**
* Build the login-response extractor bound to a particular assertion XML.
* Assertion-scoped fields are re-rooted at the assertion fragment via the
* `shortcut` mechanism so that wrapping attacks can't redirect extraction.
*
* @param assertion XML string of the (verified) assertion node
* @returns extractor fields ready for `extract`
*/
var loginResponseFields = function (assertion) { return [
{
key: 'conditions',
localPath: ['Assertion', 'Conditions'],
attributes: ['NotBefore', 'NotOnOrAfter'],
shortcut: assertion,
},
{
key: 'response',
localPath: ['Response'],
attributes: ['ID', 'IssueInstant', 'Destination', 'InResponseTo'],
},
{
key: 'audience',
localPath: ['Assertion', 'Conditions', 'AudienceRestriction', 'Audience'],
attributes: [],
shortcut: assertion,
},
{
key: 'issuer',
localPath: ['Assertion', 'Issuer'],
attributes: [],
shortcut: assertion,
},
{
key: 'nameID',
localPath: ['Assertion', 'Subject', 'NameID'],
attributes: [],
shortcut: assertion,
},
{
key: 'sessionIndex',
localPath: ['Assertion', 'AuthnStatement'],
attributes: ['AuthnInstant', 'SessionNotOnOrAfter', 'SessionIndex'],
shortcut: assertion,
},
{
key: 'attributes',
localPath: ['Assertion', 'AttributeStatement', 'Attribute'],
index: ['Name'],
attributePath: ['AttributeValue'],
attributes: [],
shortcut: assertion,
},
]; };
exports.loginResponseFields = loginResponseFields;
/** Default extractor fields for an inbound `LogoutRequest`. */
exports.logoutRequestFields = [
{
key: 'request',
localPath: ['LogoutRequest'],
attributes: ['ID', 'IssueInstant', 'Destination'],
},
{
key: 'issuer',
localPath: ['LogoutRequest', 'Issuer'],
attributes: [],
},
{
key: 'nameID',
localPath: ['LogoutRequest', 'NameID'],
attributes: [],
},
{
key: 'sessionIndex',
localPath: ['LogoutRequest', 'SessionIndex'],
attributes: [],
},
{
key: 'signature',
localPath: ['LogoutRequest', 'Signature'],
attributes: [],
context: true,
},
];
/** Default extractor fields for an inbound `LogoutResponse`. */
exports.logoutResponseFields = [
{
key: 'response',
localPath: ['LogoutResponse'],
attributes: ['ID', 'Destination', 'InResponseTo'],
},
{
key: 'issuer',
localPath: ['LogoutResponse', 'Issuer'],
attributes: [],
},
{
key: 'signature',
localPath: ['LogoutResponse', 'Signature'],
attributes: [],
context: true,
},
];
/**
* Evaluate the given extractor fields against an XML document and return
* a flat object keyed by `field.key`. Handles:
* - multi-path localPaths (`string[][]`) collected with `|`
* - parent/child attribute aggregation (`index` + `attributePath`)
* - whole-subtree extraction (`context: true`)
* - single/multiple/zero-attribute text extraction
*
* @param context XML string to parse
* @param fields extractor field definitions
* @returns extracted SAML values keyed by field name
*/
function extract(context, fields) {
var dom = (0, api_1.getContext)().dom;
var rootDoc = dom.parseFromString(context);
return fields.reduce(function (result, field) {
var key = field.key, localPath = field.localPath, attributes = field.attributes, isEntire = field.context, shortcut = field.shortcut, index = field.index, attributePath = field.attributePath;
var targetDoc = rootDoc;
if (shortcut) {
targetDoc = dom.parseFromString(shortcut);
}
// Multi-path union: each entry is a separate localPath whose text()
// values are merged.
if (localPath.every(function (path) { return Array.isArray(path); })) {
var multiXPaths = localPath
.map(function (path) { return "".concat(buildAbsoluteXPath(path), "/text()"); })
.join(' | ');
result[key] = (0, utility_1.uniq)(toNodeArray((0, xpath_1.select)(multiXPaths, targetDoc))
.map(function (n) { return n.nodeValue; })
.filter(utility_1.notEmpty));
return result;
}
var baseXPath = buildAbsoluteXPath(localPath);
var attributeXPath = buildAttributeXPath(attributes);
// Parent/child aggregation (e.g. SAML Attribute → AttributeValue).
if (index && attributePath) {
var indexPath = buildAttributeXPath(index);
var fullLocalXPath = "".concat(baseXPath).concat(indexPath);
var parentNodes = toNodeArray((0, xpath_1.select)(baseXPath, targetDoc));
var parentAttributes = toNodeArray((0, xpath_1.select)(fullLocalXPath, targetDoc)).map(function (n) { return n.value; });
var childXPath = buildAbsoluteXPath([(0, utility_1.last)(localPath)].concat(attributePath));
var childAttributeXPath = buildAttributeXPath(attributes);
var fullChildXPath_1 = "".concat(childXPath).concat(childAttributeXPath);
var childAttributes = parentNodes.map(function (node) {
var nodeDoc = dom.parseFromString(node.toString());
if (attributes.length === 0) {
var childValues = toNodeArray((0, xpath_1.select)(fullChildXPath_1, nodeDoc)).map(function (n) { return n.nodeValue; });
return childValues.length === 1 ? childValues[0] : childValues;
}
if (attributes.length > 0) {
var childValues = toNodeArray((0, xpath_1.select)(fullChildXPath_1, nodeDoc)).map(function (n) { return n.value; });
return childValues.length === 1 ? childValues[0] : childValues;
}
return null;
});
result[key] = (0, utility_1.zipObject)(parentAttributes, childAttributes, false);
return result;
}
// Whole-subtree capture.
if (isEntire) {
var nodes = toNodeArray((0, xpath_1.select)(baseXPath, targetDoc));
var value = null;
if (nodes.length === 1) {
value = nodes[0].toString();
}
else if (nodes.length > 1) {
value = nodes.map(function (n) { return n.toString(); });
}
result[key] = value;
return result;
}
// Multi-attribute capture: produce one record per parent node.
if (attributes.length > 1) {
var baseNode = toNodeArray((0, xpath_1.select)(baseXPath, targetDoc)).map(function (n) { return n.toString(); });
var childXPath_1 = "".concat(buildAbsoluteXPath([(0, utility_1.last)(localPath)])).concat(attributeXPath);
var attributeValues = baseNode.map(function (node) {
var nodeDoc = dom.parseFromString(node);
return toNodeArray((0, xpath_1.select)(childXPath_1, nodeDoc)).reduce(function (r, n) {
r[(0, utility_1.camelCase)(n.name)] = n.value;
return r;
}, {});
});
result[key] = (attributeValues.length === 1 ? attributeValues[0] : attributeValues);
return result;
}
// Single-attribute capture.
if (attributes.length === 1) {
var fullPath = "".concat(baseXPath).concat(attributeXPath);
var attributeValues = toNodeArray((0, xpath_1.select)(fullPath, targetDoc)).map(function (n) { return n.value; });
result[key] = attributeValues[0];
return result;
}
// Zero-attribute capture: element text content.
if (attributes.length === 0) {
var attributeValue = null;
var nodes = toNodeArray((0, xpath_1.select)(baseXPath, targetDoc));
if (nodes.length === 1) {
var fullPath = "string(".concat(baseXPath).concat(attributeXPath, ")");
var strResult = (0, xpath_1.select)(fullPath, targetDoc);
// `string(...)` always evaluates to a string in XPath 1.0, but the
// type signature is wider — the alternative branches are defensive.
/* v8 ignore next 7 */
attributeValue =
typeof strResult === 'string'
? strResult
: strResult === null
? null
: Array.isArray(strResult)
? strResult
: null;
}
if (nodes.length > 1) {
attributeValue = nodes
.filter(function (n) { return n.firstChild; })
.map(function (n) { return n.firstChild.nodeValue; });
}
result[key] = attributeValue;
return result;
}
/* v8 ignore next */
return result;
}, {});
}
//# sourceMappingURL=extractor.js.map