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!