Sunday, March 3, 2013

Google Maps API for Business: Signing a URL in ColdFusion (Example)

This is one of those problems that quickly sucked up a majority of my day... I wanted to use ColdFusion to make HTTP geocoding requests to the Google Maps API using my employer's business license for the service. Try as I might, using ColdFusion's encryption functions to mimic the other examples kept returning garbage.

But then it dawned on me, the Java example could be mimicked to the T in ColdFusion, since it runs on the Java platform!

<cffunction name="signGoogleURL">  
      <cfargument name="url" />  
      <!--- I stored the key in the request scope, you can remove the default attribute --->   
      <cfargument name="key" default="#request.GoogleAPIs.mapsV3.cryptoKey#" />  
      <cfset var local = {} />  
      <cfif Find(".googleapis.com", arguments.url) EQ 0>  
           <cfthrow message="Invalid Google URL." />  
      </cfif>  
      <!--- pull off the googleapis piece of the URL --->   
      <cfset local.urlToEncrypt =   
           Right(arguments.url,   
                Len(arguments.url)-(Find(".googleapis.com", arguments.url)+14)  
           )   
      />  
      <!--- this is the part we encrypt and pass as signature --->   
      <cfset local.msg_digest = cfhmac(local.urlToEncrypt, webSafeToBase64(arguments.key)) />  
      <cfreturn arguments.url & "&signature="   
           & Base64ToWebSafe(local.msg_digest) />  
 </cffunction>  
 <!--- this function mimmicks the code in signRequest from the Java example --->  
 <cffunction name="CFHMAC" output="false">  
   <cfargument name="signMsg" type="string" required="true" />  
   <cfargument name="signKey" type="string" required="true" />  
   <cfset var local = {} />  
   <!--- get the key in binary --->  
   <cfset local.key = BinaryDecode(arguments.signKey, "base64") />  
   <!--- initialize the crypto object with the key and algorithm --->  
   <cfset local.keySpec = createObject("java", "javax.crypto.spec.SecretKeySpec").init(local.key, "HmacSHA1") />  
   <cfset local.mac = createObject("java", "javax.crypto.Mac").getInstance("HmacSHA1") />  
   <cfset local.mac.init(local.keySpec) />  
   <!--- pass the message's bytes into the crypto object. we return it in Base64 --->  
   <cfreturn ToBase64(  
             local.mac.doFinal(arguments.signMsg.getBytes())  
        ) />  
 </cffunction>  
 <!--- simple switch from google's webSafe format to the actual Base64, in the Java example this is done in UrlSigner() --->   
 <cffunction name="webSafeToBase64">  
      <cfargument name="val" />  
      <cfset arguments.val = replace(  
                replace(arguments.val, "-", "+", "ALL")  
           , "_", "/", "ALL") />  
      <cfreturn arguments.val />  
 </cffunction>  
 <!--- switch back to google's format, before adding to URL. In the Java example this is done at the bottom of signRequest() --->  
 <cffunction name="Base64ToWebSafe">  
      <cfargument name="val" />  
      <cfset arguments.val = replace(  
                replace(arguments.val, "+", "-", "ALL")  
           , "/", "_", "ALL") />  
      <cfreturn arguments.val />  
 </cffunction>  
I chose to include these in a component, and call it as follows:
<cfset variables.requestURL = Maps.signGoogleUrl([url], [API CryptoKey]) />  
The url variable is the full googleapis address including the protocol, and URL parameters like the required client variable. You can then pass that into a cfhttp call and your requests will be validated by your client and cryptoKey values! Hope this saves somebody some time, let me know if you find it useful!

2 comments:

  1. This saved us sooo much time and we are very grateful that you posted this. WestsideRentals.com is the site that my fellow engineer Chintan and I maintain. We were looking through the Google API docs and could not figure out the signature part, until we found your CFC. Thank you Jacob, we are both grateful to you.

    ReplyDelete
  2. I am working with CF7 and wasn't having much luck, so I was very happy to have found this. Many thanks!

    ReplyDelete