The Signature
This is where all of the pieces come together... the libraries & keystore from Part 1 and the XML from Part 2. Here's the script that applies all of the algorithms and transforms:
<cfscript>
// 1) Injest the XML
samlAssertionElement = samlAssertionXML.getDocumentElement();
samlAssertionDocument = samlAssertionElement.GetOwnerDocument();
samlAssertion = samlAssertionDocument.getFirstChild();
// 2) Create Java Objects
SignatureSpecNS = CreateObject("Java", "org.apache.xml.security.utils.Constants").SignatureSpecNS;
TransformsClass = CreateObject("Java","org.apache.xml.security.transforms.Transforms");
SecInit = CreateObject("Java", "org.apache.xml.security.Init").Init().init();
XMLSignatureClass = CreateObject("Java", "org.apache.xml.security.signature.XMLSignature"); // Access XML Signature CLASS
CanonicalizerClass = CreateObject("Java", "org.apache.xml.security.c14n.Canonicalizer"); // Access Canonicalizer CLASS
// 3) Signature
sigType = XMLSignatureClass.ALGO_ID_SIGNATURE_RSA_SHA1; // Define signature algorithm
canonType = CanonicalizerClass.ALGO_ID_C14N_EXCL_OMIT_COMMENTS; // Define canonicalizer algorithm
signature = XMLSignatureClass.init(samlAssertionDocument, javacast("string",""), sigType, canonType); // Create signature and apply algorithms
// 4) Transforms
TransformsClass = CreateObject("Java","org.apache.xml.security.transforms.Transforms"); // Access Transforms CLASS
IncNSClass = CreateObject("Java","org.apache.xml.security.transforms.params.InclusiveNamespaces"); // Access Inclusive Namespaces CLASS
transformEnvStr = TransformsClass.TRANSFORM_ENVELOPED_SIGNATURE;
transformOmitCommentsStr = TransformsClass.TRANSFORM_C14N_EXCL_OMIT_COMMENTS;
transformIncNS = IncNSClass.init(samlAssertionDocument, "ds saml xs xsi");
transforms = TransformsClass.init(samlAssertionDocument); // Apply the base transform
transforms.addTransform(transformEnvStr); // Apply enveloped signature
transforms.addTransform(transformOmitCommentsStr); // Apply comment exclusion
transforms.item(1).getElement().appendChild(transformIncNS.getElement()); // Apply namespaces
// 5) Keystore
ksfile = CreateObject("Java", "java.io.File").init("#FULL_PATH_OF_KEYSTORE#");
inputStream = CreateObject("Java", "java.io.FileInputStream").init(ksfile);
KeyStoreClass = CreateObject("Java" , "java.security.KeyStore");
ks = KeyStoreClass.getInstance("JKS");
ks.load(inputStream,"#STOREPASS#");
keypw = "#STOREPASS#";
key = ks.getKey("#KEYSTORE_ALIAS#",keypw.toCharArray()); // Must be the alias. Using the keystore filename returns an error. This is the private key.
cert = ks.getCertificate("#KEYSTORE_ALIAS#"); // Must be the alias. Using the keystore filename returns an error.
publickey = cert.getPublicKey();
// 6) Putting it Together
signature.addDocument("#ssouser.uuid#", transforms);
signature.addKeyInfo(variables.cert); // adds certificate to the signature
signature.addKeyInfo(variables.publickey); // ads public key to signature
signature.sign(key);
samlAssertionElement.insertBefore(signature.getElement(),samlAssertion.getFirstChild()); // Takes samlAssertionElement and inserts the signature before the first node of saml:Assertion
</cfscript>
1) The first part of this script is for injesting the SAML XML code. Then you're going to traverse the DOM to the location where the signature will be inserted. In this case, it will be the first child node of
2) This portion is accessing various classes from the XML Security Libraries that were installed in Part 1.
3) Same as #2, but accessing signature related algorithms.
4) Same as #2, but accessing various transforms to apply.
5) This part is accessing the various parts of the keystore also created in Part 1.
6) The last part is adding more unique data to the signature. The unique user ID is thrown in along with the certificate and public key.
If you understood all of those steps... or even if you didn't, this should give you a sense of why SAML is super secure! Now I'm going to clean things up a bit:
<cfset samlAssertionXML = "#Replace(samlAssertionXML, "<?xml version=""1.0"" encoding=""UTF-8""?>", "", "all")#">
<cfset samlAssertionXML = "#Replace(samlAssertionXML, " xmlns:ds=""http://www.w3.org/2000/09/xmldsig##""", "", "all")#">
<cfset samlAssertionXML = "#Replace(samlAssertionXML, "<ds:Signature>", "<ds:Signature xmlns:ds=""http://www.w3.org/2000/09/xmldsig##"">", "all")#">
<cfset samlAssertionXML = "#responseOpen##samlAssertionXML##responseClose#">
1) The first line is removing the XML declaration because I have it hardcoded into the first piece of the XML already.
2) The next line is removing the namespace declaration because somewhere along the lines, in one of the Java methods, the declaration is getting added to every tag. You may not need to do this step, but I just implemented it because I wanted to get the XML as close as possible to my SP's XML spec.
3) The 3rd line just adds the namespace declaration back to <DS:SIGNATURE> tag.
4) The last line combines the 3 variables back together so that "samlAsertionXML" contains the entire XML document.
To get this ready for transport, the next step is to convert the XML to Base64...
<cfset samlAssertionXML = "#toBase64(toString(samlAssertionXML), "utf-8")#">
And finally, this can be sent over to the SP with an auto-submit form...
<form action="#SPService#" method="post">
<input type="hidden" name="SAMLResponse" value="#samlAssertionXML#" />
<input type="submit" value="Send to SP>
</form>
7 comments:
So playing the part of the SP, would I just pretty much do everything backwards? Do you have any articles that would help me out as I'm an SP trying to be a member of a IdP's federation to allow them access to my SaaS system.
Hello Jazzu-
I looked back at the references I used and the one guy (Phil Duba) that documented the SP portion doesn't have his URL anymore. It seems that most of us ColdFusion developers worked on the IDP side. But you're right. It's essentially the reverse of this. You have to work closely with the IDP to make sure you're decoding in the same way they are encoding. The SP side just receives the encoded piece, decodes and checks to see if it's a match. If it passes, then you simply allow the user in.
-Kerwin
Has anyone used this code with Google Apps? Google "can't parse the response". Working with Google on it but just wanted to check here too.
P.S. I have ColdFusion code to decode the SAML Response from Google Apps. If anyone needs it let me know.
Hi Panman,
Please send me the CF code. I would like to a look at the CF code to decode Google Apps SAML. Thank You.
-Ale
Hi Ale, I posted the code at CodeRanch. I did run into a random EOL inflation error that I never got back to but it might be related to my environment. And I never did get my SAML Response working with Google Apps, if you do please let me know! Thanks
I'm curious why you say you couldn't get out of the first level.
I started with your posted code as a basis and went back to a single part xml. It worked fine for me (CF9).
Thanks for your post
@Ryan: Not sure why I was running into that problem, but I was using an unpatched CFMX7 at the time. CF9 ironed out a lot of bugs.
@All: Thanks for your comments on this series! I'm going to close further discussion on these 3 SAML items because they're really old now. Also, that was with an old position that I've moved on from.
Post a Comment