UNPKG

samlify

Version:

Node.js library for SAML SSO

778 lines 43.4 kB
"use strict"; /** * @file libsaml.ts * @author tngan * @desc SAML primitives: templates, XML signing/verification, assertion * encryption/decryption, and XPath helpers used by the higher-level flows. */ var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var utility_1 = __importStar(require("./utility")); var urn_1 = require("./urn"); var xpath_1 = require("xpath"); var node_rsa_1 = __importDefault(require("node-rsa")); var xml_crypto_1 = require("xml-crypto"); var xmlenc = __importStar(require("@authenio/xml-encryption")); var api_1 = require("./api"); var xml_escape_1 = __importDefault(require("xml-escape")); var crypto = __importStar(require("crypto")); var fs = __importStar(require("fs")); var signatureAlgorithms = urn_1.algorithms.signature; var digestAlgorithms = urn_1.algorithms.digest; var certUse = urn_1.wording.certUse; var urlParams = urn_1.wording.urlParams; /** Coerce the heterogeneous return 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 []; } var libSaml = function () { /** * Map a SAML URL parameter type onto its canonical query-string key * (`SAMLRequest` or `SAMLResponse`). * * @param type SAML URL parameter name * @returns `SAMLRequest` or `SAMLResponse` */ function getQueryParamByType(type) { if ([urlParams.logoutRequest, urlParams.samlRequest].indexOf(type) !== -1) { return 'SAMLRequest'; } if ([urlParams.logoutResponse, urlParams.samlResponse].indexOf(type) !== -1) { return 'SAMLResponse'; } throw new Error('ERR_UNDEFINED_QUERY_PARAMS'); } /** * Mapping from XML-DSig signature algorithm URIs to node-rsa schemes. * * The PSS entry covers the redirect-binding detached signature path: * `node-rsa` accepts `pss-sha256` directly as a `signingScheme` value. * The `xmldsig-more#sha256-rsa-MGF1` URI (W3C Note, 2007-05) is listed * in `xmldsig-core §6.4.2` as the recommended successor to PKCS#1 v1.5 * RSA-SHA256 (`saml-sec-consider §6.5`). */ var nrsaAliasMapping = { 'http://www.w3.org/2000/09/xmldsig#rsa-sha1': 'pkcs1-sha1', 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256': 'pkcs1-sha256', 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512': 'pkcs1-sha512', 'http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1': 'pss-sha256', }; /** * RSASSA-PSS plugin class for `xml-crypto`'s `SignedXml.SignatureAlgorithms` * registry (`xmldsig-core §6.4.2`). * * **Temporary shim.** xml-crypto already implements PSS-SHA256 on its * master branch (PR node-saml/xml-crypto#488, merged 2025-10-17), but * the latest published npm version (6.1.2, 2024-08) predates that * commit. Once xml-crypto cuts a release containing #488, delete this * class and the `registerPssAlgorithms` helper below — the URI alone * in `algorithms.signature` is sufficient and the constructor will * seed `SignatureAlgorithms` with the upstream implementation. * * `RSA_PKCS1_PSS_PADDING` with `RSA_PSS_SALTLEN_DIGEST` matches the * MGF1+SHA-256 convention referenced by the `sha256-rsa-MGF1` URI * (xmldsig-more, W3C Note 2007-05). */ var pssPaddingOptions = { padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST, }; var RsaSha256Mgf1 = /** @class */ (function () { function RsaSha256Mgf1() { this.getSignature = function (signedInfo, privateKey) { var signOpts = { key: privateKey, padding: pssPaddingOptions.padding, saltLength: pssPaddingOptions.saltLength, }; return crypto.sign('RSA-SHA256', Buffer.from(signedInfo), signOpts).toString('base64'); }; this.verifySignature = function (material, key, signatureValue) { var verifyOpts = { key: key, padding: pssPaddingOptions.padding, saltLength: pssPaddingOptions.saltLength, }; return crypto.verify('RSA-SHA256', Buffer.from(material), verifyOpts, Buffer.from(signatureValue, 'base64')); }; this.getAlgorithmName = function () { return 'http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1'; }; } return RsaSha256Mgf1; }()); /** * Register the RSASSA-PSS SHA-256 signature class on a fresh `SignedXml` * instance so the algorithm-agility surface from `xmldsig-core §6.4` is * available for both signing and verification. PKCS#1 v1.5 entries are * preserved untouched. */ function registerPssAlgorithms(sig) { sig.SignatureAlgorithms = __assign(__assign({}, sig.SignatureAlgorithms), { 'http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1': RsaSha256Mgf1 }); } /** * Default AuthnRequest XML template. * * Per saml-core §3.4.1, `ProtocolBinding`, `AssertionConsumerServiceURL`, * and `AssertionConsumerServiceIndex` are all `use="optional"` and * mutually exclusive — when the index is set, neither URL nor * ProtocolBinding may be present. All three are placeholders here so * that `replaceTagsByValue` drops whichever the caller leaves * undefined (closes #437). */ var defaultLoginRequestTemplate = { context: '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" ForceAuthn="{ForceAuthn}" IssueInstant="{IssueInstant}" Destination="{Destination}" ProtocolBinding="{ProtocolBinding}" AssertionConsumerServiceURL="{AssertionConsumerServiceURL}" AssertionConsumerServiceIndex="{AssertionConsumerServiceIndex}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:NameIDPolicy Format="{NameIDFormat}" AllowCreate="{AllowCreate}"/></samlp:AuthnRequest>', }; /** * Default LogoutRequest XML template. * * The optional `<samlp:SessionIndex>` element (saml-core §3.7.1) is * included with a placeholder body. When the caller leaves * `user.sessionIndex` undefined, `replaceTagsByValue` drops the whole * element from the rendered XML, matching the schema's * `minOccurs="0"` declaration. */ var defaultLogoutRequestTemplate = { context: '<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}"><saml:Issuer>{Issuer}</saml:Issuer><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID><samlp:SessionIndex>{SessionIndex}</samlp:SessionIndex></samlp:LogoutRequest>', }; /** Default AttributeStatement XML fragment template. */ var defaultAttributeStatementTemplate = { context: '<saml:AttributeStatement>{Attributes}</saml:AttributeStatement>', }; /** Default Attribute XML fragment template. */ var defaultAttributeTemplate = { context: '<saml:Attribute Name="{Name}" NameFormat="{NameFormat}"><saml:AttributeValue xmlns:xs="{ValueXmlnsXs}" xmlns:xsi="{ValueXmlnsXsi}" xsi:type="{ValueXsiType}">{Value}</saml:AttributeValue></saml:Attribute>', }; /** Default LoginResponse XML template. */ var defaultLoginResponseTemplate = { context: '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status><saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{AssertionID}" Version="2.0" IssueInstant="{IssueInstant}"><saml:Issuer>{Issuer}</saml:Issuer><saml:Subject><saml:NameID Format="{NameIDFormat}">{NameID}</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="{SubjectConfirmationDataNotOnOrAfter}" Recipient="{SubjectRecipient}" InResponseTo="{InResponseTo}"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="{ConditionsNotBefore}" NotOnOrAfter="{ConditionsNotOnOrAfter}"><saml:AudienceRestriction><saml:Audience>{Audience}</saml:Audience></saml:AudienceRestriction></saml:Conditions>{AuthnStatement}{AttributeStatement}</saml:Assertion></samlp:Response>', attributes: [], additionalTemplates: { attributeStatementTemplate: defaultAttributeStatementTemplate, attributeTemplate: defaultAttributeTemplate, }, }; /** Default LogoutResponse XML template. */ var defaultLogoutResponseTemplate = { context: '<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status></samlp:LogoutResponse>', }; /** * Map a SAML signature algorithm URI to its node-rsa signing scheme. * * - When `sigAlg` is omitted, the default RSA-SHA256 scheme is used * (per `saml-bindings §3.4.4.1` recommendation). * - When `sigAlg` is supplied but does not match a known URI, the * function throws. Silently downgrading to RSA-SHA1 (the previous * behaviour) was a verification-time vulnerability: an attacker * could supply an unknown `SigAlg` query parameter to coerce * verification onto SHA-1, which is collision-broken * (`saml-sec-consider §6.5`, `xmldsig-core §6.4`). * * @param sigAlg signature algorithm URI * @returns node-rsa signing scheme string * @throws when `sigAlg` is supplied and does not match a supported URI */ function getSigningScheme(sigAlg) { if (sigAlg === undefined) { return nrsaAliasMapping[signatureAlgorithms.RSA_SHA256]; } var algAlias = nrsaAliasMapping[sigAlg]; if (algAlias === undefined) { throw new Error('ERR_UNSUPPORTED_SIGNATURE_ALGORITHM'); } return algAlias; } /** * Return the companion digest URI for a given signature algorithm URI. * * @param sigAlg signature algorithm URI * @returns digest algorithm URI or undefined when unsupported */ function getDigestMethod(sigAlg) { return digestAlgorithms[sigAlg]; } /** * Build an XPath expression that matches either a named element or one of * its attributes. * * @param local element name, or `{ name, attr }` for an attribute selector * @param isExtractAll when true the element selector resolves to its text() * @returns XPath expression */ function createXPath(local, isExtractAll) { if ((0, utility_1.isString)(local)) { var escaped = (0, utility_1.escapeXPathValue)(local); return isExtractAll === true ? '//*[local-name(.)=' + escaped + ']/text()' : '//*[local-name(.)=' + escaped + ']'; } var _a = local, name = _a.name, attr = _a.attr; return '//*[local-name(.)=' + (0, utility_1.escapeXPathValue)(name) + ']/@' + attr; } /** * Capitalise a content string after camel-casing and optionally prefix it. */ function tagging(prefix, content) { var camelContent = (0, utility_1.camelCase)(content); return prefix + camelContent.charAt(0).toUpperCase() + camelContent.slice(1); } /** * Replacer for {@link replaceTagsByValue}. Always XML-escapes the * replacement text, in both attribute and element-text contexts, to * prevent SAML attribute/element injection through user-controlled * template values. */ function escapeTag(replacement) { return function (_match, quote) { var text = replacement === null || replacement === undefined ? '' : String(replacement); return quote ? "".concat(quote).concat((0, xml_escape_1.default)(text)) : (0, xml_escape_1.default)(text); }; } return { createXPath: createXPath, getQueryParamByType: getQueryParamByType, defaultLoginRequestTemplate: defaultLoginRequestTemplate, defaultLoginResponseTemplate: defaultLoginResponseTemplate, defaultAttributeStatementTemplate: defaultAttributeStatementTemplate, defaultAttributeTemplate: defaultAttributeTemplate, defaultLogoutRequestTemplate: defaultLogoutRequestTemplate, defaultLogoutResponseTemplate: defaultLogoutResponseTemplate, /** * Substitute `{Tag}` placeholders inside an XML template with the given * replacement map. Replacement text is XML-escaped in both attribute and * element-text positions to prevent SAML element/attribute injection. * * When a tag's value is `null` or `undefined`: * - in **attribute position** (`name="{Tag}"`) the entire attribute is * omitted from the rendered XML, per `saml-core §3.4.1` (every * `<AuthnRequest>` attribute is `use="optional"`); previously samlify * emitted `name=""` or the literal `name="undefined"`, which is * invalid for typed attributes (e.g. `AssertionConsumerServiceURL` * declared as `xs:anyURI`). * - in **element-only-body position** (`<X>{Tag}</X>` or * `<X attr="...">{Tag}</X>`) the entire element is dropped — per * `saml-core §3.7.1` for `<samlp:SessionIndex>` and any other * `minOccurs="0"` element where the body is solely a placeholder. * - in **mixed-text position** (`<X>prefix{Tag}suffix</X>`) the * placeholder is replaced with the empty string (legacy behaviour). * * @param rawXML template with `{Tag}` placeholders * @param tagValues replacement map keyed by tag name * @returns XML with placeholders resolved */ replaceTagsByValue: function (rawXML, tagValues) { Object.keys(tagValues).forEach(function (t) { var value = tagValues[t]; if (value === null || value === undefined) { // Drop the entire `\s+name="{Tag}"` (or single-quoted) attribute. rawXML = rawXML.replace(new RegExp("\\s+[A-Za-z_:][\\w:.-]*=(\"|')\\{".concat(t, "\\}\\1"), 'g'), ''); // Drop the entire `<X ...>{Tag}</X>` element when the placeholder // is the only body content (allows optional elements to be omitted // rather than rendered empty). rawXML = rawXML.replace(new RegExp("<([A-Za-z_:][\\w:.-]*)((?:\\s+[^>]*)?)>\\{".concat(t, "\\}</\\1>"), 'g'), ''); // Replace any remaining `{Tag}` occurrences with empty string. rawXML = rawXML.replace(new RegExp("\\{".concat(t, "\\}"), 'g'), ''); return; } rawXML = rawXML.replace(new RegExp("(\"?)\\{".concat(t, "\\}"), 'g'), escapeTag(value)); }); return rawXML; }, /** * Build a serialized `<AttributeStatement>` from attribute descriptors * by applying the attribute and statement templates. * * @param attributes attribute descriptors (name, format, value) * @param attributeTemplate per-attribute template * @param attributeStatementTemplate wrapping statement template * @returns serialized XML fragment */ attributeStatementBuilder: function (attributes, attributeTemplate, attributeStatementTemplate) { if (attributeTemplate === void 0) { attributeTemplate = defaultAttributeTemplate; } if (attributeStatementTemplate === void 0) { attributeStatementTemplate = defaultAttributeStatementTemplate; } var attr = attributes.map(function (_a) { var name = _a.name, nameFormat = _a.nameFormat, valueTag = _a.valueTag, valueXsiType = _a.valueXsiType, valueXmlnsXs = _a.valueXmlnsXs, valueXmlnsXsi = _a.valueXmlnsXsi; var defaultValueXmlnsXs = 'http://www.w3.org/2001/XMLSchema'; var defaultValueXmlnsXsi = 'http://www.w3.org/2001/XMLSchema-instance'; var attributeLine = attributeTemplate.context; attributeLine = attributeLine.replace('{Name}', name); attributeLine = attributeLine.replace('{NameFormat}', nameFormat); attributeLine = attributeLine.replace('{ValueXmlnsXs}', valueXmlnsXs ? valueXmlnsXs : defaultValueXmlnsXs); attributeLine = attributeLine.replace('{ValueXmlnsXsi}', valueXmlnsXsi ? valueXmlnsXsi : defaultValueXmlnsXsi); attributeLine = attributeLine.replace('{ValueXsiType}', valueXsiType); attributeLine = attributeLine.replace('{Value}', "{".concat(tagging('attr', valueTag), "}")); return attributeLine; }).join(''); return attributeStatementTemplate.context.replace('{Attributes}', attr); }, /** * Compute an XML-DSig signature over the supplied SAML message. Can * sign the message root (`isMessageSigned`), a referenced subtree * (`referenceTagXPath`), or both. * * @param opts signature inputs and layout options * @returns base64 (default) or raw signed XML string */ constructSAMLSignature: function (opts) { var rawSamlMessage = opts.rawSamlMessage, referenceTagXPath = opts.referenceTagXPath, privateKey = opts.privateKey, privateKeyPass = opts.privateKeyPass, _a = opts.signatureAlgorithm, signatureAlgorithm = _a === void 0 ? signatureAlgorithms.RSA_SHA256 : _a, _b = opts.transformationAlgorithms, transformationAlgorithms = _b === void 0 ? [ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#', ] : _b, signingCert = opts.signingCert, signatureConfig = opts.signatureConfig, _c = opts.isBase64Output, isBase64Output = _c === void 0 ? true : _c, _d = opts.isMessageSigned, isMessageSigned = _d === void 0 ? false : _d; var sig = new xml_crypto_1.SignedXml(); // xmldsig-core §6.4.2 — make PSS variants available alongside the // PKCS#1 v1.5 set built into xml-crypto v6.x. No-op for callers // staying on RSA-SHA*; required when `signatureAlgorithm` is one of // the `xmldsig-more 2007-05` PSS URIs. registerPssAlgorithms(sig); var digestAlgorithm = getDigestMethod(signatureAlgorithm); if (referenceTagXPath) { sig.addReference({ xpath: referenceTagXPath, transforms: transformationAlgorithms, digestAlgorithm: digestAlgorithm, }); } if (isMessageSigned) { sig.addReference({ xpath: '/*', transforms: transformationAlgorithms, digestAlgorithm: digestAlgorithm, }); } sig.signatureAlgorithm = signatureAlgorithm; sig.publicCert = this.getKeyInfo(signingCert, signatureConfig).getKey(); sig.getKeyInfoContent = this.getKeyInfo(signingCert, signatureConfig).getKeyInfo; sig.privateKey = utility_1.default.readPrivateKey(privateKey, privateKeyPass, true); sig.canonicalizationAlgorithm = 'http://www.w3.org/2001/10/xml-exc-c14n#'; if (signatureConfig) { sig.computeSignature(rawSamlMessage, signatureConfig); } else { sig.computeSignature(rawSamlMessage); } return isBase64Output !== false ? utility_1.default.base64Encode(sig.getSignedXml()) : sig.getSignedXml(); }, /** * Verify an XML-DSig signature on a SAML payload and, on success, return * the cryptographically authenticated assertion node. * * Defends against classic wrapping attacks by rejecting assertions that * appear inside a `SubjectConfirmationData` subtree. * * @param xml SAML message XML * @param opts metadata or key file plus signature algorithm * @returns tuple `[verified, authenticatedAssertion | null]` */ verifySignature: function (xml, opts) { var e_1, _a; var _b; var dom = (0, api_1.getContext)().dom; var doc = dom.parseFromString(xml); var contextDom = (0, api_1.getContext)().dom; var docParser = contextDom; // Absolute XPaths defend against signature-wrapping attacks. var messageSignatureXpath = "/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Signature']"; var assertionSignatureXpath = "/*[contains(local-name(), 'Response') or contains(local-name(), 'Request')]/*[local-name(.)='Assertion']/*[local-name(.)='Signature']"; var wrappingElementsXPath = "/*[contains(local-name(), 'Response')]/*[local-name(.)='Assertion']/*[local-name(.)='Subject']/*[local-name(.)='SubjectConfirmation']/*[local-name(.)='SubjectConfirmationData']//*[local-name(.)='Assertion' or local-name(.)='Signature']"; var selection = []; var messageSignatureNode = toNodeArray((0, xpath_1.select)(messageSignatureXpath, doc)); var assertionSignatureNode = toNodeArray((0, xpath_1.select)(assertionSignatureXpath, doc)); var wrappingElementNode = toNodeArray((0, xpath_1.select)(wrappingElementsXPath, doc)); selection = selection.concat(messageSignatureNode); selection = selection.concat(assertionSignatureNode); if (wrappingElementNode.length !== 0) { throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK'); } if (selection.length === 0) { return [false, null]; } var _loop_1 = function (signatureNode) { var sig = new xml_crypto_1.SignedXml(); // Register PSS plugins on the verifier instance so the algorithm // declared inside the signed XML can resolve to a SignatureAlgorithm // class (xmldsig-core §6.4.2; see `registerPssAlgorithms`). registerPssAlgorithms(sig); var verified = false; sig.signatureAlgorithm = opts.signatureAlgorithm; if (!opts.keyFile && !opts.metadata) { throw new Error('ERR_UNDEFINED_SIGNATURE_VERIFIER_OPTIONS'); } if (opts.keyFile) { sig.publicCert = fs.readFileSync(opts.keyFile); } if (opts.metadata) { var certificateNode = toNodeArray((0, xpath_1.select)(".//*[local-name(.)='X509Certificate']", signatureNode)); var metadataCert = opts.metadata.getX509Certificate(certUse.signing); if (Array.isArray(metadataCert)) { metadataCert = (0, utility_1.flattenDeep)(metadataCert); } else if (typeof metadataCert === 'string') { metadataCert = [metadataCert]; } metadataCert = metadataCert.map(utility_1.default.normalizeCerString); if (certificateNode.length === 0 && metadataCert.length === 0) { throw new Error('NO_SELECTED_CERTIFICATE'); } if (certificateNode.length !== 0) { var certEl = certificateNode[0]; var x509CertificateData = (_b = certEl.textContent) !== null && _b !== void 0 ? _b : ''; var x509Certificate_1 = utility_1.default.normalizeCerString(x509CertificateData); if (metadataCert.length >= 1 && !metadataCert.find(function (cert) { return cert.trim() === x509Certificate_1.trim(); })) { throw new Error('ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA'); } sig.publicCert = this_1.getKeyInfo(x509Certificate_1).getKey(); } else { sig.publicCert = this_1.getKeyInfo(metadataCert[0]).getKey(); } } sig.loadSignature(signatureNode); verified = sig.checkSignature(doc.toString()); if (!verified) { return "continue"; } if (!(sig.getSignedReferences().length >= 1)) { throw new Error('NO_SIGNATURE_REFERENCES'); } var signedVerifiedXML = sig.getSignedReferences()[0]; var rootNode = docParser.parseFromString(signedVerifiedXML, 'text/xml').documentElement; if (rootNode.localName === 'Response') { var assertions = toNodeArray((0, xpath_1.select)("./*[local-name()='Assertion']", rootNode)); var encryptedAssertions = toNodeArray((0, xpath_1.select)("./*[local-name()='EncryptedAssertion']", rootNode)); if (assertions.length === 1) { return { value: [true, assertions[0].toString()] }; } else if (encryptedAssertions.length >= 1) { return { value: [true, rootNode.toString()] }; } return { value: [true, null] }; } else if (rootNode.localName === 'Assertion') { return { value: [true, rootNode.toString()] }; } return { value: [true, null] }; }; var this_1 = this; try { for (var selection_1 = __values(selection), selection_1_1 = selection_1.next(); !selection_1_1.done; selection_1_1 = selection_1.next()) { var signatureNode = selection_1_1.value; var state_1 = _loop_1(signatureNode); if (typeof state_1 === "object") return state_1.value; } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (selection_1_1 && !selection_1_1.done && (_a = selection_1.return)) _a.call(selection_1); } finally { if (e_1) throw e_1.error; } } return [false, null]; }, /** * Build the metadata `<KeyDescriptor>` fragment for a certificate use. * * @param use `signing` or `encryption` * @param certString PEM certificate body or Buffer * @returns element tree consumable by the `xml` module */ createKeySection: function (use, certString) { return { KeyDescriptor: [ { _attr: { use: use }, }, { 'ds:KeyInfo': [ { _attr: { 'xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', }, }, { 'ds:X509Data': [{ 'ds:X509Certificate': utility_1.default.normalizeCerString(certString), }], }, ], }, ], }; }, /** * Produce a detached RSA signature over a SAML redirect-binding octet * string. See SAML bindings spec §3.4.4.1. * * @param octetString canonical query-string to sign * @param key PEM private key * @param passphrase optional passphrase for the key * @param isBase64 when true (default), base64-encode the signature * @param signingAlgorithm signature algorithm URI * @returns base64 string (default) or raw Buffer signature */ constructMessageSignature: function (octetString, key, passphrase, isBase64, signingAlgorithm) { var decryptedKey = new node_rsa_1.default(utility_1.default.readPrivateKey(key, passphrase), undefined, { signingScheme: getSigningScheme(signingAlgorithm), }); var signature = decryptedKey.sign(octetString); return isBase64 !== false ? signature.toString('base64') : signature; }, /** * Verify a detached RSA signature over a redirect-binding octet string. * * @param metadata peer metadata carrying the signing certificate * @param octetString canonical query-string that was signed * @param signature signature bytes * @param verifyAlgorithm signature algorithm URI (optional) * @returns true when the signature verifies */ verifyMessageSignature: function (metadata, octetString, signature, verifyAlgorithm) { var signCert = metadata.getX509Certificate(certUse.signing); var signingScheme = getSigningScheme(verifyAlgorithm); var key = new node_rsa_1.default(utility_1.default.getPublicKeyPemFromCertificate(signCert), 'public', { signingScheme: signingScheme }); return key.verify(Buffer.from(octetString), Buffer.from(signature)); }, /** * Build the KeyInfo XML fragment and PEM public key for a certificate. * * @param x509Certificate certificate body (no PEM wrappers) * @param signatureConfig optional prefix/location for the KeyInfo element */ getKeyInfo: function (x509Certificate, signatureConfig) { if (signatureConfig === void 0) { signatureConfig = {}; } var prefix = signatureConfig.prefix ? "".concat(signatureConfig.prefix, ":") : ''; return { getKeyInfo: function () { return "<".concat(prefix, "X509Data><").concat(prefix, "X509Certificate>").concat(x509Certificate, "</").concat(prefix, "X509Certificate></").concat(prefix, "X509Data>"); }, getKey: function () { return utility_1.default.getPublicKeyPemFromCertificate(x509Certificate).toString(); }, }; }, /** * Encrypt the `<Assertion>` inside a SAML response using the target * entity's encryption certificate. Returns the base64-encoded XML * containing the `<EncryptedAssertion>` element in place of the plaintext. * * @param sourceEntity entity initiating the encryption (its settings drive the algorithms) * @param targetEntity entity whose certificate is used * @param xml response XML containing a single `<Assertion>` * @returns promise resolving to base64-encoded XML */ encryptAssertion: function (sourceEntity, targetEntity, xml) { return new Promise(function (resolve, reject) { if (!xml) { return reject(new Error('ERR_UNDEFINED_ASSERTION')); } var sourceEntitySetting = sourceEntity.entitySetting; var targetEntityMetadata = targetEntity.entityMeta; var dom = (0, api_1.getContext)().dom; var doc = dom.parseFromString(xml); var assertions = (0, xpath_1.select)("//*[local-name(.)='Assertion']", doc); if (!Array.isArray(assertions) || assertions.length === 0) { throw new Error('ERR_NO_ASSERTION'); } if (assertions.length > 1) { throw new Error('ERR_MULTIPLE_ASSERTION'); } var rawAssertionNode = assertions[0]; if (sourceEntitySetting.isAssertionEncrypted) { var encryptCert = targetEntityMetadata.getX509Certificate(certUse.encrypt); var publicKeyPem = utility_1.default.getPublicKeyPemFromCertificate(encryptCert); xmlenc.encrypt(rawAssertionNode.toString(), { rsa_pub: Buffer.from(publicKeyPem), pem: Buffer.from("-----BEGIN CERTIFICATE-----".concat(encryptCert, "-----END CERTIFICATE-----")), encryptionAlgorithm: sourceEntitySetting.dataEncryptionAlgorithm, keyEncryptionAlgorithm: sourceEntitySetting.keyEncryptionAlgorithm, }, function (err, res) { /* v8 ignore start */ if (err) { console.error(err); return reject(new Error('ERR_EXCEPTION_OF_ASSERTION_ENCRYPTION')); } if (!res) { return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION')); } /* v8 ignore stop */ var encAssertionPrefix = sourceEntitySetting.tagPrefix.encryptedAssertion; var encryptAssertionDoc = dom.parseFromString("<".concat(encAssertionPrefix, ":EncryptedAssertion xmlns:").concat(encAssertionPrefix, "=\"").concat(urn_1.namespace.names.assertion, "\">").concat(res, "</").concat(encAssertionPrefix, ":EncryptedAssertion>")); doc.documentElement.replaceChild(encryptAssertionDoc.documentElement, rawAssertionNode); return resolve(utility_1.default.base64Encode(doc.toString())); }); } else { return resolve(utility_1.default.base64Encode(xml)); } }); }, /** * Decrypt the `<EncryptedAssertion>` inside a SAML response using the * local entity's private key. Returns both the decrypted document XML * and the raw assertion fragment for downstream extraction. * * @param here local entity performing decryption * @param entireXML SAML response XML containing `<EncryptedAssertion>` * @returns tuple `[decryptedDocumentXml, rawAssertionXml]` */ decryptAssertion: function (here, entireXML) { return new Promise(function (resolve, reject) { if (!entireXML) { return reject(new Error('ERR_UNDEFINED_ASSERTION')); } var hereSetting = here.entitySetting; var dom = (0, api_1.getContext)().dom; var doc = dom.parseFromString(entireXML); var encryptedAssertions = (0, xpath_1.select)("/*[contains(local-name(), 'Response')]/*[local-name(.)='EncryptedAssertion']", doc); if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) { throw new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION'); } if (encryptedAssertions.length > 1) { throw new Error('ERR_MULTIPLE_ASSERTION'); } var encAssertionNode = encryptedAssertions[0]; return xmlenc.decrypt(encAssertionNode.toString(), { key: utility_1.default.readPrivateKey(hereSetting.encPrivateKey, hereSetting.encPrivateKeyPass), }, function (err, res) { /* v8 ignore start */ if (err) { console.error(err); return reject(new Error('ERR_EXCEPTION_OF_ASSERTION_DECRYPTION')); } if (!res) { return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION')); } /* v8 ignore stop */ var rawAssertionDoc = dom.parseFromString(res); doc.documentElement.replaceChild(rawAssertionDoc.documentElement, encAssertionNode); return resolve([doc.toString(), res]); }); }); }, /** * Validate the SAML XML against the registered schema validator. Throws * when no validator has been configured via {@link setSchemaValidator} * so consumers can't silently ship without schema checks. * * @param input SAML XML string */ isValidXml: function (input) { return __awaiter(this, void 0, void 0, function () { var validate; return __generator(this, function (_a) { switch (_a.label) { case 0: validate = (0, api_1.getContext)().validate; if (!validate) { return [2 /*return*/, Promise.reject(new Error('Your application is potentially vulnerable because no validation function found. Please read the documentation on how to setup the validator. (https://github.com/tngan/samlify#installation)'))]; } return [4 /*yield*/, validate(input)]; case 1: return [2 /*return*/, _a.sent()]; } }); }); }, }; }; exports.default = libSaml(); //# sourceMappingURL=libsaml.js.map