Blog

Digest Authentication in ColdFusion

January 8, 2009 · 12 Comment s

I'm working with the ConstantContact API webservice, and ran into a slight problem. The API uses Digest Authentication, and ColdFusion does not support Digest Authentication in cfhttp. I found this out by reading in the live docs that:

The cfhttp tag does not support NTLM or Digest Authentication.

The ConstantContact forums pointed me to a custom tag cfx_http5. But I hate CFX tags. I like to place as few dependencies on the ColdFusion Administrator as possible when I code. CFX tags also tend not to make the trip the first time in server migrations or emergency moves. It might be my own prejudice but, no, there will be no custom tags.

Which leaves me to implementing some Java client, as luck would have it there was ample documentation on doing this. I just needed to use the open source Apache HTTP client. To work with them, I had to add them to the class path, or add an entry in the ColdFusion Administrator class path list. That would have bought me nothing, as I didn't want to go through the administrator.

This left me looking for a way to dynamically call jar files. If only there was some way to dynamically call jar files in ColdFusion... preferably written by someone with a marsupial pouch.... Ohh wait, Mark Mandel wrote the Open Source project JavaLoader, which allows just that. Hmm, and Mark is a mammal from Australia. Things are looking good.

So I was able to build my http client with Digest Authentication. I added some code that formatted the response like a cfhttp struct, so that if there were other advantages to this method I could just swap out this for cfhttp in a pinch. The code is below in the extended section of this blog post.

I was trying to do 3 things with this post:

  1. Explain how to use Digest Authentication with ColdFusion
  2. Show how with ColdFusion Open Source community and ColdFusion's ability to call Java, there isn't much you can't do with it.
  3. Further the rumor that Mark Mandel is a marsupial.

I hope I've done these three things for you.

<cfcomponent>
   
   <cffunction name="init" output="FALSE" access="public" returntype="any" hint="Psuedo constructor that allows us to play our object games." >
      
      <cfscript>
         var paths = arrayNew(1);
         var rootPath = GetDirectoryFromPath(GetCurrentTemplatePath());

         paths[1] = rootPath & "/jars/commons-httpclient-3.1.jar";
         paths[2] = rootPath & "/jars/commons-codec-1.3.jar";
         paths[3] = rootPath & "/jars/commons-logging-1.1.1.jar";

         //create the loader
         variables.loader = createObject("component", "javaloader.JavaLoader").init(paths);
      </cfscript>
      <cfreturn This />
   </cffunction>
   
   <cffunction name="get" output="FALSE" access="public" returntype="struct" hint="Runs a get request." >
      <cfargument name="url" type="string" required="TRUE" hint="The url to call." />
      <cfargument name="username" type="string" default="" hint="The username to pass to the service." />
      <cfargument name="password" type="string" default="" hint="The password to pass to the service." />
      <cfargument name="realm" type="string" default="" hint="The realm to pass to the service." />
      <cfargument name="port" type="numeric" default="80" hint="The port to pass to the service." />
      
      <cfscript>
      
         var result = "";
         var credentials = loader.create("org.apache.commons.httpclient.UsernamePasswordCredentials");
         var HttpClient = loader.create("org.apache.commons.httpclient.HttpClient").init();
         var httpGet = loader.create("org.apache.commons.httpclient.methods.GetMethod");
         var AuthScope = loader.create("org.apache.commons.httpclient.auth.AuthScope");
         var jURL = createObject("java", "java.net.URL").init(arguments.url);
         
         if (len(arguments.username) and len(arguments.password) gt 0){
            AuthScope.init(jURL.getHost(), arguments.port, arguments.realm);
            credentials.init(arguments.Username, arguments.password);
            httpClient.getState().setCredentials(authScope, credentials);
         }
         
         httpGet.init(arguments.url);
         httpClient.executeMethod(httpGet);
         
         result = convertHttpClientResponseToCFHTTPFormat(httpGet);
         httpGet.releaseConnection();
      
         return result;
      </cfscript>
      
   </cffunction>

   <cffunction name="convertHttpClientResponseToCFHTTPFormat" output="FALSE" access="public" returntype="struct" hint="Takes the response from the HHTP client and formats like cfhttp." >
      <cfargument name="httpGet" type="any" required="TRUE" hint="The httpGet Java Object that was called." />   
   
      <cfscript>
         var result = structNew();
         var responseheader = structNew();   
         responseheader['Status_Code'] = httpGet.getStatusCode();
         responseheader['Explanation'] = httpGet.getStatusText();
         responseheader['Http_Version'] = httpGet.getEffectiveVersion().toString();
         header = httpGet.getStatusLine().toString();
         
         headers = httpGet.getResponseHeaders();
         
         for (i=1; i lte ArrayLen(headers); i=i+1){
            responseheader[getToken(headers[i], 1, ":")] = getToken(headers[i], 2, ":");
            header = listAppend(header, headers[i], " ");
         }
         
         result['Charset'] = httpGet.getResponseCharSet();
         result['ErrorDetail'] = "";
         result['Filecontent'] = httpGet.getResponseBodyAsString();
         result['Header'] = header;
         
         if (structKeyExists(responseheader, 'Content-Type')){
            result['Mimetype'] = GetToken(responseheader['Content-Type'], 1, ";");
         }
         
         result['Responseheader'] = responseheader;
         result['Statuscode'] = responseheader['Status_Code'] & " " & responseheader['Explanation'];
         
         if(   not structKeyExists(responseheader, 'Content-Type') OR
            FindNoCase("text", responseheader['Content-Type']) OR
            FindNoCase("message", responseheader['Content-Type']) OR
            FindNoCase("application/octet-stream", responseheader['Content-Type'])
         ){
            result['Text'] = "YES";
         }
         else{
            result['Text'] = "NO";
         }   
            
   
         return result;
      </cfscript>
   
   </cffunction>

</cfcomponent>

Tags: Web Development · ColdFusion

12 response s so far ↓

  • 1 Rob Brooks-Bilson // Jan 8, 2009 at 11:31 AM

    Terry,

    Thanks for sharing. I'll be filing this one away for later.

    Oh, and you are truly a master at spreading rumors!
  • 2 Chris Diller // Jan 8, 2009 at 12:10 PM

    I always had my suspicions about Mark...
  • 3 Dave Konopka // Jan 8, 2009 at 12:41 PM

    Very nice solution. Who knew Apache could talk with the dead? Maybe only you and the marsupials.
  • 4 Sami Hoda // Jan 8, 2009 at 2:25 PM

    This should go on RiaForge!
  • 5 Terrence Ryan // Jan 8, 2009 at 2:30 PM

    @Sami, I kinda rushed it out as a response to CF is dead nonsense today. I might give that a try, but I'm looking at a swamped next few weeks.

    Anyone who wants to steal the code and release on Riaforge can be my guest.
  • 6 pat branley // Jan 8, 2009 at 3:38 PM

    Hi Guys
    you can do the same thing on windows with the XMLHTTP COM object.
    you don't need to install anything because its part of the OS.
  • 7 James // Feb 12, 2009 at 3:34 AM

    Talk about perfect timing! Decided I wanted to get my work calendar, which is hosted on an OS X Caldav server, into my Google calendar. Trouble was Google doesn't support remote calendars with authentication so I needed a CF proxy in between. This worked a treat, although for some reason I needed to put the port number in the URL string as well as the port attribute.
    Many thanks!
  • 8 Ben Margolis // Mar 18, 2009 at 11:32 AM

    I have to say great timing as well. I'm just starting to do some API programming with Constant Contact in a ColdFusion page. Make sure you use the "realm" argument with "api.constantcontact.com" as the value. Thanks for the post.
  • 9 Ben Margolis // Mar 18, 2009 at 3:08 PM

    You can do a post also, just copy the get function, replacing "get" with "post" and then:

    ...
    <cfargument name="url" type="string" required="TRUE" hint="The url to call." />
    <cfargument name="body" type="string" required="TRUE" hint="What to send to the URL" />

    ....

    var AuthScope = loader.create("org.apache.commons.httpclient.auth.AuthScope");
    var myStringRequestEntity = loader.create("org.apache.commons.httpclient.methods.StringRequestEntity");

    ....

    httpPost.init(arguments.url);
    httpPost.setRequestEntity(myStringRequestEntity.init(arguments.body, "application/atom+xml", "UTF-8"));

    ....

    NOTE: This is for posting an ATOM XML document as a string.
  • 10 .jonah // Mar 19, 2009 at 4:48 AM

    Excellent! I'm about to need a constant contact API wrapper myself. If it's alright w/you, I'll use your code as a starting point and release it and the CC api on riaforge once they're done.
  • 11 Ben Margolis // Jul 17, 2009 at 3:49 PM

    Constant Contact's API is switching from Digest Authentication to Basic Authentication over HTTPS. Since you don't indicate the authentication type (digest versus basic), it appears there's nothing that needs to change in the code. We just need to make sure the URLs have "https" instead of "http." I'll update you if anything changes after I test this strategy out.
  • 12 Ben Margolis // Aug 4, 2009 at 11:02 AM

    You need to change default="80" to default="443" in order to use https correctly. That, and changing the URL, will do the trick.

Leave a Comment