SAML, SSO and ColdFusion

Overview

The following post is intended as an overview of how a clients IT department could integrate their intranet with our CMD system to provide a seamless single sign on. There are commercial systems that can do this and we have integrated with some of them. The following shows how to use the open source Apache XMLSecurity library to the same effect.

For an overview of SSO and SAML see my earlier posting. In this post I will illustrate an implementation using the SAML Post Profile, the Apache XML Security library and ColdFusion. (Since the Apache library is a java library, the JSP/java code is almost identical). There are two parts: the creation of the SAML on the intranet server (IdP) and the interpretation of this by an external service, or to use the correct terminology, service provider (SP).

Configuring the Idp

This article assumes that you have ColdFusion 8. Alternatively you could run Tomcat and use JSP although the syntax is different, the library calls are the same.

This implementation uses the Apache XML security library. Download and place in the WEB-INF/cfusion/lib directory for ColdFusion (for java into your applications lib directory), You probably already have many of the jar files and just need serializer.jar and xmlsec-nnn.jar, where nnn is the current version number.

Creating the SAML on the IdP

The starting point is a SAML assertion from the previous article. We need to add some real user data and time information. The users identity and other information to embed in the SAML would come from Active Directory, LDAP etc. For this example I’ve hard coded some values.

<cfscript>
    ssouser=StructNew();
    ssouser["email"]=”user@tagworldwide.com”;
    nowDateTime =
        #DateFormat(DateConvert(‘local2utc’,
          Now()),’YYYY-MM-DDT’)#
        & #TimeFormat(DateConvert(‘local2utc’,
          Now()),’HH:mm:SSZ’)#;
    nowDateTimePlus1 =
        #DateFormat(DateConvert(‘local2utc’,
          DateAdd(‘n’,1,Now())),’YYYY-MM-DDT’)#
        & #TimeFormat(DateConvert(‘local2utc’,
          DateAdd(‘n’,1,Now())),’HH:mm:SSZ’)#;
</cfscript>

The CFXML tag provides an easy way to populate the SAML

<cfoutput>
<cfxml variable=”samlAssertionXML”>
<samlp:Response
  xmlns:ds=http://www.w3.org/2000/09/xmldsig#chr(35)#
  xmlns:saml=”urn:oasis:names:tc:SAML:1.0:assertion” 
  xmlns:samlp=”urn:oasis:names:tc:SAML:1.0:protocol” 
  IssueInstant=”#nowDateTime#”
  MajorVersion=”1″ MinorVersion=”1″ 
  Recipient=”http://externalService/sso/post/
  ResponseID=”#CreateUUID()#”>
    <samlp:Status>
        <samlp:StatusCode Value=”samlp:Success”/>
    </samlp:Status>
    <saml:Assertion
        xmlns:saml=”urn:oasis:names:tc:SAML:1.0:assertion”
        AssertionID=”#AssertionID#”
        IssueInstant=”#nowDateTime#”
        Issuer=”#issuer#” MajorVersion=”1″
        MinorVersion=”1″>
        <saml:Conditions NotBefore=”#nowDateTime#”
            NotOnOrAfter=”#nowDateTimePlus1#”>
            <saml:AudienceRestrictionCondition>
                <saml:Audience>
                  http://intranet/IdP
                </saml:Audience>
            </saml:AudienceRestrictionCondition>
        </saml:Conditions>
        <saml:AuthenticationStatement 
           AuthenticationInstant=”2007-11-04T13:52:42Z”>
           <saml:Subject>
             <saml:NameIdentifier>#ssouser.id#
             </saml:NameIdentifier>
           </saml:Subject>
        </saml:AuthenticationStatement>
        <saml:AttributeStatement>
         <saml:Attribute AttributeName=”emailaddress”>
         <saml:AttributeValue>#ssouser.email#
         </saml:AttributeValue> 
         </saml:Attribute>
         <saml:Attribute AttributeName=”givenname”>
         <saml:AttributeValue>
             #ssouser.givenname#
           </saml:AttributeValue> 
         </saml:Attribute>
         <saml:Attribute AttributeName=”surname”>  
         <saml:AttributeValue>
         #ssouser.surname#
         </saml:AttributeValue> 
         </saml:Attribute>
        </saml:AttributeStatement>
    </saml:Assertion>
</samlp:Response>
</cfxml>
</cfoutput>

The above uses an authentication statement for the users identity and attribute statements for additional attributes e.g. users full name, email address etc. The standard doesn’t define which attributes are used. It allows namespace attributes to be used, allowing complete flexibility.

To add a signature element, we use the XMLSecurity library. The ColdFusion variables need coaxing to extract the right java objects. The last line does the actual insertion of a signature element into the SAML.

<cfscript>
samlAssertionElement = 
  samlAssertionXML.getDocumentElement();
samlAssertionDocument =
  samlAssertionElement.GetOwnerDocument();
samlAssertion = samlAssertionDocument
  .getFirstChild();
SignatureSpecNS = CreateObject(“Java”,
  “org.apache.xml.security.utils.Constants”)
  .SigSpecNS;
Init = CreateObject(“Java”, 
   “org.apache.xml.security.Init”).Init().init();
XMLSignatureClass = CreateObject(“Java”, 
   “org.apache.xml.security.signature.XMLSignature”);
sigType =
   XMLSignatureClass.ALGO_ID_SIGNATURE_RSA_SHA1;
signature = XMLSignatureClass
   .init(samlAssertionDocument, 
    javacast(“string”,”"), sigType); 
samlAssertionElement
  .insertBefore(signature 
  .getElement(),samlAssertion.getFirstChild());

</cfscript>

Note that the signature is inside the the XML that we are signing; this is known as an enveloped signature. The following is needed by the XMLSecurity library to define that we will be using an enveloped signature.

TransformsClass = CreateObject(“Java”,
“org.apache.xml.security.transforms.Transforms”);
transformEnvStr =  
TransformsClass.TRANSFORM_ENVELOPED_SIGNATURE;
transformOmitCommentsStr =
TransformsClass.TRANSFORM_C14N_EXCL_OMIT_COMMENTS;
transforms = TransformsClass
.init(samlAssertionDocument
transforms.addTransform(transformOmitCommentsStr);
transforms.addTransform(transformEnvStr);

To sign the SAML, we need a certificate. A self self certificate can be used. Typically issuer signed certificates are used, and for this the cheapest certificate from Thawte etc is sufficient. Java typically stores these in a key store. You can follow one of the numerous guides to doing this via the command line or use Portecle which is very easy to use. The code to extract a certificate from a key store is as follows

ksfile = CreateObject(“Java”, “java.io.File”)
  .init(storepath);
inputStream = CreateObject(“Java”,
  “java.io.FileInputStream”)
  .init(ksfile);
KeyStoreClass = CreateObject(“Java”
  , “java.security.KeyStore”);        
ks = KeyStoreClass.getInstance(“JKS”); 
ks.load(inputStream,”sso”); 
key = ks.getKey(“sso”,keypw.toCharArray());
cert = ks.getCertificate(certAlias);
publickey = cert.getPublicKey();

We now need to add a digest, signature and key information.

signature.addDocument(“#chr(35)##AssertionID#”
  , transforms);
signature.addKeyInfo(cert);
signature.addKeyInfo(publickey);
signature.sign(key);

In this example we have signed the whole response. You will see examples on the web where first the saml assertion is signed, and then in addition the response is signed as well. This isn’t required when the assertion and response are created at the same time.

Finally, encode this into a HTML page that will do a post to the SP.

<html>
    <body onload=”document.forms[0].submit()”>
        <form action=
http://ServiceProvider/SSO/POST/” method=”post”>
        <input type=”hidden” name=”SAMLRequest”
        value=’#BinaryEncode(
           CharsetDecode(
           samlAssertionXML,”utf-8″)
           ,”Base64″)#’/>
        </form>
    </body>
</html>

Interpreting the XML on the External Service (SP)

On the Service Provider, we do everything in reverse.

First extract the SAML, parse the XML, and verify the signature. There are a few lines of code to set up the various java objects correctly; the last line does the work.

xmlResponse=CharsetEncode(
  BinaryDecode(form.SAMLRequest,”Base64″)
  ,”utf-8″);
docElement= XmlParse(variables.xmlResponse)
  .getDocumentElement();
SignatureConstants=CreateObject(
  “Java”, “org.apache.xml.security.utils.Constants”);
SignatureSpecNS=SignatureConstants.SignatureSpecNS; 
xmlSignatureClass = CreateObject(“Java”,
  “org.apache.xml.security.signature.XMLSignature”);
xmlSignature = xmlSignatureClass 
  .init(docElement.getElementsByTagNameNS
  (SignatureSpecNS,”Signature”)
  .item(0),”");
keyInfo=xmlSignature.getKeyInfo();
X509CertificateResolverCN = 
  “org.apache.xml.security.keys.keyresolver
  .implementations.X509CertificateResolverClass”).
keyResolver=CreateObject(“Java”,
   X509CertificateResolverCN)
  .init();
keyInfo.registerInternalKeyResolver(keyResolver);
x509cert = keyInfo.getX509Certificate();

isValid = xmlSignature.checkSignatureValue(x509cert);

Assuming the SAML has been correctly signed, we now need to establish that the request is from an approved source, is in the right time frame and isn’t a repeat. Finally extract the users identity.

ssoissuer = getSingleValue(XPathIssuer);
conditions = getSingleValue(XPathConditions);
beforeDate = DateConvertISO8601
  (conditions.XmlAttributes.NotBefore, 0);
afterDate = DateConvertISO8601
  (conditions.XmlAttributes.NotOnOrAfter, afterwindow);
certificate = getSingleValue
  (XPathCertificates).XmlText;
reference = getSingleValue(XPathUniqueReference);

// Verify Validity Info
// …

   // Extract User
   ssouser =  getSingleValue(XPathNameIdentifier).xmltext;

Much of the additional code for validity, verifying whether its a new or existing user etc will be very much application dependant.

The extras

The code above uses the function DateConvertISO8601, an implementation of this from David Satz can be found on cflib. The function getSingleValue is a wrapper around XMLSearch.

Finally, the class org.apache.xml.security.Init has an init initializer method that clashes with the ColdFusion init method; in CF 7 you need to call it via java introspection as in the following snippet (thanks to Jon for the correction on this):

InitClass = CreateObject(“Java”,
     “org.apache.xml.security.Init”);
emptyArray = ArrayNew(1);
initMethod=InitClass.GetClass()
  .GetMethod(“init”,emptyArray);
initMethod.invoke(Init,emptyArray);

<cffunction name=”getSingleValue” output=”false”
   access=”public” returntype=”ANY”>
   <!— Expects an array of values to be returned
   from XML data, but only returns the first one —>
   <cfargument name=”xpath” required=”true”>
   <cfargument name=”reqAttrib” required=”No”
     type=”boolean” default=”true”>
    <cfset values=
        XmlSearch(variables.xmlResponse,
        arguments.xpath)>
        <cfif reqAttrib >
         <cfif ArrayLen(values) EQ 0>
           <cfthrow type=”saml”
           message=”Error: No value for: #xpath#”>
        </cfif>
        </cfif>
        <cfif isArray(values)>
            <cfreturn values[1]>
        <cfelse>
            <cfreturn values >
        </cfif>
</cffunction>

More Reading

This PDF has a very clear java implementation. Phil Duba has a set of well written SAML articles on his blog. This uses a wrapper round a java implementation. You may also need to know about the differences between SAML1.1 and SAML 2.0: see saml.xml.org.

10 Responses to “SAML, SSO and ColdFusion”

  1. Jon Clausen says:

    David,

    Great post. Your walkthrough and Phil Duba’s SAML and Coldfusion series have been lifesavers in trying to implement an SSO. Not to make you debug my code or anything, but I’ve tried both Phil’s and your method and in both cases seem to be throwing an instantiation error when attempting to pass the samlAssertionDocument into the constructor of the XMLSignatureClass.

    Looking at the javadocs, the constructor for the XMLSignature class seems to be getting the correct types passed in but throws “The class must not be an interface or an abstract class”

    Any thoughts or lessons learned in your work on this which might provide me a clue?

    Thanks.

  2. Jon Clausen says:

    Update: My problem was related to instantiating the org.apache.xml.security.Init object with createObject(). There’s a known issue in the way that CF handles Init().

    I simply changed:

    var Init = CreateObject(“Java”, “org.apache.xml.security.Init”).Init()

    to

    var Init = CreateObject(“Java”, “org.apache.xml.security.Init”).Init().init()

    and the problem was solved.

    Thanks again for you work on this. It was a lifesaver.

  3. Justin says:

    Hi David,

    I recently found your article while researching Coldfusion & SAML examples online. I’m in the middle of trying to put together a SAML Consumer-only SSO scenerio for a site that runs on CF MX 7. I noticed your steps assume CF 8, but nothing I’ve seen in there so far looks implicit to 8, except possibly some of the really lower-level Java stuff that I’m not quite as familliar with. As far as you know, will your example run on MX as well, or are there known road-blocks there?

    While implementing your SAML Consumer/decryption code as a test, I keep getting tripped up on the “keyInfo” field, which CF MX claims is never defined, thus the getX509Certificate() and final isValid() calls against that object fail.

    The one part that’s a little unclear to me is where in the flow of the process the init override code snippit has to fall? I’ve written a saml.cfc file that has a comsume() method in it that handles the POST, and within there, have implemented your code. I just pasted the init block right at the top of that function, and it doesn’t seem to be complaining…

    Anyhow… any insight into the CF MX vs. 8 issue would be appreciated. It’d be nice to know I’m not beating my head against a wall I cannot overcome :)

    -Justin

  4. David Rutter says:

    Hi Justin,

    You can use this in CF7 – our original implementation was on CF7.
    We had to use some introspection code as follows

    Init = CreateObject(“Java”, “org.apache.xml.security.Init”);
    emptyArray = ArrayNew(1);
    initMethod=Init.GetClass().GetMethod(“init”,emptyArray);
    initMethod.invoke(Init,emptyArray);

    Jon’s suggestion is obviously the better way but I think this is CF8 only.

    var Init = CreateObject(”Java”, “org.apache.xml.security.Init”).Init().init()

  5. Jon Clausen says:

    @David,@Justin

    - Guys actually, my suggestion works on CF8 and CF7. My dev server is CF8 and the production server is still CF7.

    Here’s the full content of my signing function:

    var samlAssertionXML=arguments.document;
    var xmlout = “”;
    //Create our Signing Objects from XMLSEC
    var samlAssertionElement = samlAssertionXML.getDocumentElement();
    var samlAssertionDocument = samlAssertionElement.GetOwnerDocument();
    var samlAssertion = samlAssertionDocument.getFirstChild();
    var SignatureSpecNS = CreateObject(“Java”, “org.apache.xml.security.utils.Constants”).SignatureSpecNS;
    var TransformsClass = CreateObject(“Java”,”org.apache.xml.security.transforms.Transforms”);
    var SecInit = CreateObject(“Java”, “org.apache.xml.security.Init”).Init().init();
    var XMLSignatureClass = CreateObject(“Java”, “org.apache.xml.security.signature.XMLSignature”);
    var sigType = XMLSignatureClass.ALGO_ID_SIGNATURE_DSA;
    var signature = XMLSignatureClass.init(samlAssertionDocument, toString(“”), sigType);
    //transform class vars
    var transformEnvStr = “”;
    var transformOmitCommentsStr =”";
    var transforms =”";
    //Insert signature
    samlAssertionElement
    .insertBefore(signature
    .getElement(),samlAssertion.getFirstChild());
    //transform document for XMLSEC into enveloped signature
    transformEnvStr =
    TransformsClass.TRANSFORM_ENVELOPED_SIGNATURE;
    transformOmitCommentsStr =
    TransformsClass.TRANSFORM_C14N_EXCL_WITH_COMMENTS;
    transforms = TransformsClass.init(samlAssertionDocument);
    transforms.addTransform(transformOmitCommentsStr);
    transforms.addTransform(transformEnvStr);
    //Scope our Cert and key variables
    getAuthCert();
    //Now add our signature
    signature.addDocument(“#chr(35)##controller.getBean(“AssertionID”)#”
    , transforms);
    signature.addKeyInfo(variables.cert);
    signature.addKeyInfo(variables.publickey);
    signature.sign(variables.key);
    xmlout = samlAssertionXML;
    return xmlout;

    HTH,
    Jon

  6. Jon Clausen says:

    @all – sorry about the formatting, looks like the comment system stripped out the tags the above is inside a cfscript block. The cffunction tag has one argument, document whichi is the SAM assertion XML object.

  7. Justin says:

    Well, in one of those “is it plugged in?” kinda moments, I just realized I’m trying to process a SAML 2.0 packet with your SAML 1.0 processing code, so that’s probably the source of my consistent failures :)

    I must not be getting the correct XML elements passed into the certificate validation functions because of the differences in protocols.

    Back to the docs I go!

  8. Howie says:

    is this portion of code correct?
    transforms = TransformsClass
    .init(samlAssertionDocument
    transforms.addTransform(transformOmitCommentsStr);

    I changed it to

    transforms = TransformsClass.init(samlAssertionDocument);
    transforms.addTransform(transformOmitCommentsStr);
    transforms.addTransform(transformEnvStr);

    and got past that error but now i am getting
    Cannot resolve element with ID 940A010B-188B-73FA-E68F75D98735111E
    on the signature.sign(key);
    which is probably unrelated

  9. CFFusionDev says:

    Update… I still need code as mentioned in my earlier post and I am getting error when using current code below

    xmlResponse= tokenXML;

    docElement= XmlParse(variables.xmlResponse).getDocumentElement();

    SignatureConstants=CreateObject(“Java”, “org.apache.xml.security.utils.Constants”);

    SignatureSpecNS=SignatureConstants.SignatureSpecNS;

    xmlSignatureClass = CreateObject(“Java”,”org.apache.xml.security.signature.XMLSignature”);

    xmlSignature = xmlSignatureClass.init(docElement.getElementsByTagNameNS(SignatureSpecNS,”Signature”).item(0),”"); // this lines gives error. see details below
    //Exceptions 13:00:53.053 – Object Exception – in **** Object Instantiation //Exception.

    keyInfo=xmlSignature.getKeyInfo();

    X509CertificateResolverCN = CreateObject(“Java”,”org.apache.xml.security.keys.keyresolver.implementations.X509CertificateResolverClass”);

    keyResolver=CreateObject(“Java”,X509CertificateResolverCN).init();

    keyInfo.registerInternalKeyResolver(keyResolver);

    x509cert = keyInfo.getX509Certificate();

    For clarification purpose, its this line that throws error.

    xmlSignature = xmlSignatureClass.init(docElement.getElementsByTagNameNS(SignatureSpecNS,”Signature”).item(0),”");

    I am trying to find solution but if you have suggestions please email me at CFFusionDev AT Gmail Dot Com.

    Thanks,

  10. CFFusionDev says:

    Hi there,

    I have my code running thanks to your blog. :-)

    One question about line
    isValid = xmlSignature.checkSignatureValue(x509cert);

    isValid returns *NO* in my case. Only thing different from your case is that assertion sent to me is not Base64 Encoded which means I dont
    use this line from your code on SP side
    xmlResponse=CharsetEncode(BinaryDecode(form.SAMLRequest,”Base64″)
    ,”utf-8″);

    Does that make any difference or you think it has to do with certificate in ds:X509Certificate element meaning this public key/certificate is different than what was used for signing?

    Thanks,

Leave a Reply