Tuesday, February 26, 2013

Authenticating to OAuth Services Using AdWords Scripts

Many APIs today use OAuth in order for users to authenticate and call their service.  One very popular example is the Twitter REST API.  Digging through the AdWords Scripting reference, you'll notice that one of the cool things you can do with AdWords Scripts is to pull data from a URL using the UrlFetchApp.

So the question I asked was "Could I write a script to authenticate using OAuth and retrieve data from the Twitter REST API?"

Obviously, the first place I looked was in the OAuthConfig class of the UrlFetchApp.  I set up the configuration and tried to make a call but I kept getting an authentication error.  A little research showed that I wasn't the only one having this problem.  The issue stems from the fact that normally, users need to authenticate through some sort of dialog box before they can access data. See this example about connecting to Picasa Web Albums for more details.

But then I found a post about someone managing to get around this by re-implementing the OAuth authentication system in their script and using a standard UrlFetchApp.fetch() request.

I was up for a challenge so I recreated it myself.  It turned out to be quite a bit of code, but in the end, the only thing you really need to worry about is getting the correct keys from Twitter and calling _build_authorization_string().

Thanks,
Russ

//-----------------------------------
// Authenticate and Connect to OAuth Service
// Created By: Russ Savage
// FreeAdWordsScripts.com
//-----------------------------------
function main() {
  //Define the Twitter Keys and Secrets
  //More info on obtaining these can be found at https://dev.twitter.com/docs/auth/tokens-devtwittercom
  var oauth_key_stuff = {
    "consumer_key" : "your consumer key",
    "consumer_secret" : "your consumer secret",
    "access_token" : "your access token",
    "access_token_secret" : "your access token secret"
  };

  // Update this with the REST url you want to call.  I only tested it with GET
  // but i don't think there is anything stopping a POST request from working.
  var url_stuff = {  
    "http_method" : 'GET',
    "base_url" : "https://api.twitter.com/1.1/statuses/user_timeline.json"
  };

  //Add the parameters for the REST url you want to call.
  var url_param_stuff = {
    "screen_name" : "russellsavage" //hey that's me!
  };
  
  // Don't touch this stuff
  var other_oauth_data = {
    "oauth_nonce" : Utilities.base64Encode(Math.random() +
          "secret_sauce" +
          (new Date()).getTime()).replace(/(?!\w)/g, ''),
    "oauth_signature_method" : "HMAC-SHA1",
    "oauth_timestamp" : Math.round((new Date()).getTime() / 1000.0),
    "oauth_version" : "1.0"
  };
  
  // Here is where the magic happens
  var auth_string = _build_authorization_string(oauth_key_stuff,url_stuff,url_param_stuff,other_oauth_data);

  var options = {
    "headers" : { "Authorization" :  auth_string }
  };
    
  var url = _build_url(url_stuff,url_param_stuff);
  var response = UrlFetchApp.fetch(url, options);
  var tweets = JSON.parse(response.getContentText());
  
  //now let's log my amazing tweets!
  for(var tweet in tweets) {
    var t = tweets[tweet];
    Logger.log(t.text);
  }
 
  // HELPER FUNCTIONS BELOW
  
  function _build_url(base_url,param_stuff){
    var url = base_url.base_url;
    if(param_stuff != {}) {
      url += '?';
    }
    for(var key in param_stuff) {
      url += key + "=";
      url += encodeURIComponent(param_stuff[key]);
      url += '&';
    }
    return url.slice(0,-1);
  }
  
  function _build_param_string(auth_keys,url_data,oauth_data) {
    var data_for_param_string = {
      "oauth_consumer_key" : auth_keys.consumer_key,
      "oauth_nonce" : oauth_data.oauth_nonce,
      "oauth_signature_method" : oauth_data.oauth_signature_method,
      "oauth_timestamp" : oauth_data.oauth_timestamp,
      "oauth_token" : auth_keys.access_token,
      "oauth_version" : oauth_data.oauth_version
    };
    
    // add additional url values
    for(var my_key in url_data) { 
      data_for_param_string[my_key] = url_data[my_key]; 
    }
    
    // find and sort the keys for later
    var keys = [];
    for(var key in data_for_param_string) {
      keys.push(key);
    }
    keys.sort();
    
    //finally build and return the param string
    var param_string = "";
    for(var i in keys) {
      param_string += keys[i] + "=" + encodeURIComponent(data_for_param_string[keys[i]]);
      if(i < keys.length - 1) {
        param_string += "&";
      }
    }
    
    return param_string;
  }
  
  function _build_sig_base_string(my_url_stuff,my_param_string) {
    return my_url_stuff.http_method +
      "&" + encodeURIComponent(my_url_stuff.base_url) +
      "&" + encodeURIComponent(my_param_string);
  }
  
  function _build_sigining_key(my_key_stuff) {
    return encodeURIComponent(my_key_stuff.consumer_secret) + 
      "&" + encodeURIComponent(my_key_stuff.access_token_secret);
  }
  
  function _build_oauth_signature(base_string,sign) {
    return Utilities.base64Encode(
      Utilities.computeHmacSignature(
        Utilities.MacAlgorithm.HMAC_SHA_1, 
        base_string, 
        sign
      )
    );
  }
  
  function _build_authorization_string(my_key_stuff,my_url_stuff,my_url_param_stuff,my_oauth_stuff) {
    var param_string = _build_param_string(my_key_stuff,my_url_param_stuff,my_oauth_stuff);
    var sig_base_string = _build_sig_base_string(my_url_stuff,param_string);
    var signing_key = _build_sigining_key(my_key_stuff);
    var oauth_signature = _build_oauth_signature(sig_base_string,signing_key);
    return "OAuth " +
           encodeURIComponent("oauth_consumer_key") + '="' + 
             encodeURIComponent(my_key_stuff.consumer_key) + '", ' +
           encodeURIComponent("oauth_nonce") + '="' + 
             encodeURIComponent(my_oauth_stuff.oauth_nonce) + '", ' +
           encodeURIComponent("oauth_signature") + '="' + 
             encodeURIComponent(oauth_signature) + '", ' +
           encodeURIComponent("oauth_signature_method") + '="' + 
             encodeURIComponent(my_oauth_stuff.oauth_signature_method) + '", ' +
           encodeURIComponent("oauth_timestamp") + '="' + 
             encodeURIComponent(my_oauth_stuff.oauth_timestamp) + '", ' +
           encodeURIComponent("oauth_token") + '="' + 
             encodeURIComponent(my_key_stuff.access_token) + '", ' +
           encodeURIComponent("oauth_version") + '="' + 
             encodeURIComponent(my_oauth_stuff.oauth_version) + '"';
    
  }
  
}