Skip to content

Instantly share code, notes, and snippets.

@oddlyfunctional
Last active February 13, 2020 09:02
Show Gist options
  • Save oddlyfunctional/e9448ead07cb7bd72be7d91925851da8 to your computer and use it in GitHub Desktop.
Save oddlyfunctional/e9448ead07cb7bd72be7d91925851da8 to your computer and use it in GitHub Desktop.
/*
* Usage:
$ node decode.js https://test-idp1.gakunin.nii.ac.jp/idp/profile/SAML2/Redirect/SSO?SAMLRequest=nVPBjtowEP2VyPckNgvqyiKsKKgq0rYbkWwPe6mMMyxuEzv1THbp39cJUNFqlwOnWDPPzzPvvUzv9k0dvYBH42zGRMLZ3WyKqqlbOe9oZ9fwqwOkKMAsyqGRsc5b6RQalFY1gJK0LOZf7uUo4bL1jpx2NYvmiOAp8C6cxa4BX4B%2FMRoe1%2FcZ2xG1KNOUAnmMrUie1c%2FOGptYYxKlkx9tWuzMZuNqoF2C6NL%2BhVGaPxQli5bhlrGKhqH%2FoTLVm1yhnIbJtqaGI9EaKuNBU1oUDyxaLTP2XUy4vp2Mt2IrPqgbzicCbkMLsYOVRVKWMjbiIx5zEd%2FwUnApxnLEk8l48sSi%2FLj4R2MrY58vq7Q5gFB%2BLss8Piz17eRCALCDCXJ43J%2Bpf5lWnSRns%2F8EjkWiXecRNgohHKfpGf%2FJ8a%2BBcLXMXW3072sc%2F%2BR8o%2Bgyuq%2BYKt4OUNn2OyOBpZCXunavCw%2BKIGPkO2DpabBjCqEaMhnyRLC%2FKpML17TKG%2Bxlhr3SdBL6nHhRBx3XsL1G9oswLXVPHcp5%2BLw6X%2FWhCSGEqvTKYus8HY15a57ZofeOHH%2B75%2F%2Ft7A8%3D
<?xml version="1.0"?><samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="https://test-sp1.gakunin.nii.ac.jp/Shibboleth.sso/SAML2/POST" Destination="https://test-idp1.gakunin.nii.ac.jp/idp/profile/SAML2/Redirect/SSO" ID="_150c854f1f17a30051e8" IssueInstant="2020-01-30T10:14:20.545Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://test-sp-1.coursebase.co</saml:Issuer><samlp:NameIDPolicy xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" AllowCreate="true"/><samlp:RequestedAuthnContext xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Comparison="exact"><saml:AuthnContextClassRef xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext></samlp:AuthnRequest>
*/
const zlib = require('zlib');
const decode = url =>
zlib.inflateRawSync(
Buffer.from(
new URL(url).searchParams.get('SAMLRequest'),
"base64"
)
).toString();
console.log(decode(process.argv[2]));
/*
* Usage:
* node decodeResponse.js pathToBase64EncodedSamlResponse pathToCoursebaseDecryptionPrivateKey
*/
const xmlenc = require('xml-encryption');
const xmldom = require('xmldom');
const xpath = require('xml-crypto').xpath;
const fs = require('fs');
const encoded = fs.readFileSync(process.argv[2]).toString();
const xml = Buffer.from(encoded, 'base64').toString('utf8');
const doc = new xmldom.DOMParser({}).parseFromString(xml);
const encryptedAssertions = xpath(doc,
"/*[local-name()='Response']/*[local-name()='EncryptedAssertion']");
const options = {
key: fs.readFileSync(process.argv[3]),
};
xmlenc.decrypt(encryptedAssertions[0].toString(), options, function(err, decryptedXml) {
if (err) { throw err; }
const decryptedDoc = new xmldom.DOMParser().parseFromString(decryptedXml);
doc.replaceChild(decryptedDoc, encryptedAssertions[0]);
console.log(doc.toString());
});
/*
Our original request:
<?xml version="1.0"?>
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_f6c1d5e36096162b95f7" Version="2.0" IssueInstant="2020-01-30T11:07:01.783Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Destination="https://test-idp1.gakunin.nii.ac.jp/idp/profile/SAML2/Redirect/SSO" AssertionConsumerServiceURL="https://test.coursebase.co/sso/saml">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://test.coursebase.co</saml:Issuer>
<samlp:NameIDPolicy xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" AllowCreate="true" />
<samlp:RequestedAuthnContext xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Comparison="exact">
<saml:AuthnContextClassRef xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
</samlp:RequestedAuthnContext>
</samlp:AuthnRequest>
Their original request:
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="https://test-sp1.gakunin.nii.ac.jp/Shibboleth.sso/SAML2/POST" Destination="https://test-idp1.gakunin.nii.ac.jp/idp/profile/SAML2/Redirect/SSO" ID="_db7c7f8e5fde425700f947b7837d743b" IssueInstant="2020-01-30T09:38:49Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://test-sp1.gakunin.nii.ac.jp/shibboleth-sp</saml:Issuer>
<samlp:NameIDPolicy AllowCreate="1" />
</samlp:AuthnRequest>
*/
// Below, their original request was edited the closest possible to our's.
// Everything matches, except the `AssertionConsumerServiceURL` and the
// `saml:Issuer`. Note that the `ID` and `IssueInstant` attributes must be
// dynamically generated, therefore they are instead defined here as templates
// to be replaced at the moment of usage.
const makeXml = ({ AssertionConsumerServiceURL, Issuer }) => `
<?xml version="1.0"?>
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="%ID%" Version="2.0" IssueInstant="%NOW%" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Destination="https://test-idp1.gakunin.nii.ac.jp/idp/profile/SAML2/Redirect/SSO" AssertionConsumerServiceURL="${AssertionConsumerServiceURL}">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${Issuer}</saml:Issuer>
<samlp:NameIDPolicy xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" AllowCreate="true" />
<samlp:RequestedAuthnContext xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Comparison="exact">
<saml:AuthnContextClassRef xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
</samlp:RequestedAuthnContext>
</samlp:AuthnRequest>
`;
const coursebaseSetup = {
AssertionConsumerServiceURL: 'https://test.coursebase.co/sso/saml',
Issuer: 'https://test.coursebase.co',
};
const gakuninSetup = {
AssertionConsumerServiceURL: 'https://test-sp1.gakunin.nii.ac.jp/Shibboleth.sso/SAML2/POST',
Issuer: 'https://test-sp1.gakunin.nii.ac.jp/shibboleth-sp',
};
let xml = makeXml(gakuninSetup);
// To test how the idP respond when we replace our `Issuer` and `AssertionConsumerServiceURL`, uncomment the line below
// xml = makeXml(coursebaseSetup);
const minify = xml => xml.replace(/\n\s*/g, "");
const zlib = require('zlib');
const nanoid = require('nanoid/generate');
const makeId = () => '_' + nanoid('1234567890abcdef', 20);
const encode = xml =>
zlib.deflateRawSync(
Buffer.from(
minify(xml)
.replace(/%NOW%/g, new Date().toISOString())
.replace(/%ID%/, makeId()),
"utf-8"
)
).toString("base64");
const makeUrl = saml => {
const url = new URL('https://test-idp1.gakunin.nii.ac.jp/idp/profile/SAML2/Redirect/SSO');
url.searchParams.set("SAMLRequest", saml);
return url.toString();
};
const { exec } = require('child_process');
console.log("SAMLRequest:");
console.log(xml);
const url = makeUrl(encode(xml));
console.log("URL:", url);
exec(`open "${url}"`);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment