Skip to content

Instantly share code, notes, and snippets.

@mihai-vlc
Created August 27, 2025 18:02
Show Gist options
  • Select an option

  • Save mihai-vlc/98fcc82ba783227a7f1e55117a4f3511 to your computer and use it in GitHub Desktop.

Select an option

Save mihai-vlc/98fcc82ba783227a7f1e55117a4f3511 to your computer and use it in GitHub Desktop.
const sax = require('sax');
function escapeAttr(s) {
return s.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/"/g, '&quot;')
.replace(/\t/g, '&#x9;')
.replace(/\n/g, '&#xA;')
.replace(/\r/g, '&#xD;');
}
function escapeText(s) {
return s.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\r/g, '&#xD;');
}
function isVisiblyUtilized(pref, elemPrefix, attrs) {
if (pref === '') {
return elemPrefix === '' || attrs.some(a => a.prefix === '');
} else {
return elemPrefix === pref || attrs.some(a => a.prefix === pref);
}
}
function exclusiveCanonicalize(xmlString, options = {}) {
const { withComments = false, inclusivePrefixList = [] } = options;
const parser = sax.parser(true, { trim: false, normalize: false });
const output = [];
let topLevelHasChild = false;
const stack = [];
let currentNsInScope = {};
let currentRendered = {};
let currentLastUtil = null;
parser.onopentag = (node) => {
// Extract local namespaces and attributes
const localNs = {};
const attrs = [];
for (const key in node.attributes) {
if (key === 'xmlns') {
localNs[''] = node.attributes[key];
} else if (key.startsWith('xmlns:')) {
const p = key.substring(6);
localNs[p] = node.attributes[key];
} else {
let attrPrefix = '';
let attrLocal = key;
const attrParts = key.split(':');
if (attrParts.length === 2) {
attrPrefix = attrParts[0];
attrLocal = attrParts[1];
}
attrs.push({
prefix: attrPrefix,
localName: attrLocal,
value: node.attributes[key]
});
}
}
const nsInScope = { ...currentNsInScope, ...localNs };
// Parse element name
let elemPrefix = '';
let elemLocalName = node.name;
const parts = node.name.split(':');
if (parts.length === 2) {
elemPrefix = parts[0];
elemLocalName = parts[1];
}
// Set attribute URIs
attrs.forEach(attr => {
attr.uri = nsInScope[attr.prefix] || '';
});
const tagName = elemPrefix ? elemPrefix + ':' + elemLocalName : elemLocalName;
output.push('<' + tagName);
const inclusiveSet = new Set(inclusivePrefixList);
const defaultInclusive = inclusiveSet.has('#default');
inclusiveSet.delete('#default');
let nsToRender = [];
for (const [pref, uri] of Object.entries(nsInScope)) {
if (pref === '' && uri === '') continue;
const isInclusive = pref === '' ? defaultInclusive : inclusiveSet.has(pref);
const isVisUtil = isVisiblyUtilized(pref, elemPrefix, attrs);
const renderedUri = currentRendered[pref];
if ((isInclusive || isVisUtil) && renderedUri !== uri) {
nsToRender.push({ pref, uri });
}
}
const currDefaultUri = nsInScope[''] || '';
const utilizesDefault = isVisiblyUtilized('', elemPrefix, attrs);
let addUndeclare = false;
if (currDefaultUri === '') {
if ((utilizesDefault || defaultInclusive) && currentLastUtil && currentLastUtil !== '') {
addUndeclare = true;
nsToRender.push({ pref: '', uri: '' });
}
}
nsToRender.sort((a, b) => {
if (a.pref === '') return -1;
if (b.pref === '') return 1;
return a.pref.localeCompare(b.pref);
});
for (const ns of nsToRender) {
const attrName = ns.pref === '' ? 'xmlns' : 'xmlns:' + ns.pref;
output.push(' ' + attrName + '="' + escapeAttr(ns.uri) + '"');
}
let sortedAttrs = [...attrs];
sortedAttrs.sort((a, b) => {
const uriA = a.uri || '';
const uriB = b.uri || '';
if (uriA !== uriB) return uriA.localeCompare(uriB);
return a.localName.localeCompare(b.localName);
});
for (const attr of sortedAttrs) {
const attrName = attr.prefix ? attr.prefix + ':' + attr.localName : attr.localName;
output.push(' ' + attrName + '="' + escapeAttr(attr.value) + '"');
}
output.push('>');
if (stack.length === 0) {
topLevelHasChild = true;
}
const newNsRendered = { ...currentRendered };
for (const ns of nsToRender) {
newNsRendered[ns.pref] = ns.uri;
}
let childLastUtilDefaultUri;
if (utilizesDefault) {
childLastUtilDefaultUri = addUndeclare ? '' : currDefaultUri;
} else {
childLastUtilDefaultUri = currentLastUtil;
}
// Push parent contexts to stack
stack.push({
tagName,
rendered: currentRendered,
lastUtil: currentLastUtil,
nsInScope: currentNsInScope
});
// Update current contexts
currentRendered = newNsRendered;
currentLastUtil = childLastUtilDefaultUri;
currentNsInScope = nsInScope;
};
parser.onclosetag = () => {
const popped = stack.pop();
output.push('</' + popped.tagName + '>');
currentRendered = popped.rendered;
currentLastUtil = popped.lastUtil;
currentNsInScope = popped.nsInScope;
};
parser.ontext = (text) => {
const escaped = escapeText(text);
let wrote = false;
if (stack.length > 0) {
output.push(escaped);
wrote = true;
} else if (text.trim() !== '') {
output.push(escaped);
wrote = true;
}
if (wrote && stack.length === 0) {
topLevelHasChild = true;
}
};
parser.oncomment = (comment) => {
if (withComments) {
if (stack.length === 0) {
if (topLevelHasChild) {
output.push('\n');
}
}
output.push('<!--' + comment + '-->');
if (stack.length === 0) {
topLevelHasChild = true;
}
}
};
parser.onprocessinginstruction = (pi) => {
const data = pi.body ? ' ' + pi.body : '';
if (stack.length === 0) {
if (topLevelHasChild) {
output.push('\n');
}
}
output.push('<?' + pi.name + data + '?>');
if (stack.length === 0) {
topLevelHasChild = true;
}
};
parser.onerror = (err) => {
throw err;
};
parser.write(xmlString).close();
return output.join('');
}
var res = exclusiveCanonicalize(`<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6" Version="2.0" IssueInstant="2014-07-17T01:01:48Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
<saml:Subject>
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
<saml:AudienceRestriction>
<saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
<saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>`);
console.log(res);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment