Doing OAuth authentication in Ruby with Freshbooks

I just finished a 2-day saga trying to authenticate against Freshbooks from a Ruby on Rails application. Conceptually, connecting the two isn’t that hard and Freshbooks even has a documentation page describing the whole process. That, plus the widely-used Ruby OAuth gem and the equally useful Ruby Freshbooks gem should mean that connecting is a piece of cake, right? Well, I found that it wasn’t. In fact, I found myself spending eight hours over two days trying to get all the way through mostly because the Ruby OAuth gem and Freshbooks each have quirks that make them not play well with each other.

Freshbooks has great high-level documentation about doing OAuth, but it punts on the details to the OAuth 1.0a specification. That would be fine, except that in some ways Freshbooks differs from the obvious path through the spec. While it does conform to the spec, technically, a reader of it might have to try several different options before hitting on something that works. Most critically, Freshbooks only accepts the PLAINTEXT signature method, and this method doesn’t work by default in the Ruby OAuth gem. As another (much less bothersome) example, Freshbooks does require the oauth_verifier and oauth_callback parameters even though these parameters are optional in the spec (and almost every example of using the oauth gem on the Internet omits them).

The documentation for the otherwise-awesome ruby-freshbooks gem also doesn’t help with OAuth. It simply says “note: this library provides no suppport for obtaining OAuth request or access tokens. for help with that take a look at FreshBooks’ OAuth Documentation and the oauth gem.” So, yeah, you’re on your own.

But that’s ok. I have a reasonably good understanding of what OAuth is supposed to do so I figured I could wade through, no problem. But that’s where the quirks of the Ruby OAuth gem vs. FreshBooks’ expectations tripped me up.

So, first I’ll list all the issues I had, then I’ll give you all the code you need to log in to Freshbooks and get an OAuth access token. First, all of my issues in the order I encountered them:

  1. It took me a little while to figure out how to turn on HTTP debugging in the Ruby OAuth gem so I could see what was going over the raw wire;
  2. PLAINTEXT is the only signature method accepted by Freshbooks but the Ruby OAuth gem barfs when you try to specify :signature_method=>’PLAINTEXT’ in the constructor to OAuth::Consumer.new. This is where I lost the vast majority of my time;
  3. Every example you will ever see on the Internet for the Ruby OAuth gem uses the default scheme, so I finally had to read the source code to see how to specify “:scheme=>:query_string” and get Freshbooks to see the parameters I was trying to pass to it;
  4. The OAuth gem ignores the :oauth_callback parameter if you pass it in the constructor, but luckily does use it if you pass it in to the call to get_request_token method;
  5. I thought* the OAuth gem was mangling the :access_request_path I was passing to it so I had to* use the :access_request_url instead, which led to…
  6. …specifying the :access_request_url makes the http debugging I worked so hard for disappear because the OAuth gem throws away its @http object and creates a new one when you call get_access_token
  7. It took me a while to figure out where to pass in the :oauth_verifier parameter because no examples on the Internet ever use it

*I actually had a bug in my code where I was setting :site to the wrong thing, but didn’t figure that out until I’d already worked around it by brute force.

So, without further ado, here is the code that actually works:

require 'oauth'
#note: this explicit require must be here or else oauth says PLAINTEXT is not a valid signature method
require 'oauth/signature/plaintext'

#this should be the site of your client.
base = "https://examplesite.freshbooks.com" 
# given to you by Freshbooks
key = "freshbooks-oauth-consumer-key" 
# given to you by Freshbooks
secret = "freshbooks-oauth-consumer-secret" 
# needs to be a valid path in your application
callback = "http://www.someapp.com/freshbooks-oauth-callback" 

oauth = OAuth::Consumer.new( key,secret,{:site=>base,
                      :scheme=> :query_string, #this is the only one Freshbooks accepts
                      :signature_method=>"PLAINTEXT",  # this is the only one Freshbooks accepts
                      :oauth_callback=>callback, #this seems to actually be ignored
                      :authorize_path => "/oauth/oauth_authorize.php",
                      :access_token_path=>"/oauth/oauth_access.php",
                      :request_token_path=>"/oauth/oauth_request.php"})
#this lets you see raw wire calls
oauth.http.set_debug_output($stdout) 

#this callback is not ignored and is actually used
request_token = oauth.get_request_token(:oauth_callback=>callback) 
puts request_token.inspect

#pretend like the user logged in
oauth_verifier = redirect_and_wait_for_freshbooks_response(request_token, base)
#end of pretending like the user logged in

#now, finally, get the access_token you wanted and then use it to your heart's content
access_token = request_token.get_access_token(:oauth_verifier=>oauth_verifier)
puts access_token.inspect

# and use the access_token to create a new Freshbooks client:
client = FreshBooks::Client.new(base, key, secret, access_token.token, access_token.secret)

So there it is in all its gory detail. And what about that “redirect_and_wait_for_freshbooks_response” function I hand waved my way around? That would normally be a two part process like this:

  1. in your controller, redirect the user to request_token.authorize_url
  2. Freshbooks eventually sends a request back to your “callback” url with the “oauth_verifier” parameter set

I was writing this in a unit test where I have a test Freshbooks account, so I simulated this happening by just posting that test account’s login, like so:

#here we are pretending to be the user and submitting the login form as if we were them.  In reality, you'd redirect
# the user to the url and then wait for Freshbooks to redirect them back to you.
def redirect_and_wait_for_freshbooks_response(req_token, base)
    url = "#{base}/oauth/oauth_authorize.php"
    params = {:oauth_token=>req_token.token,
              :user=>'a-test-user-name',
              :pass=>'a-test-user-password',
              :submit=>'true'}
    real_url = "#{url}?".concat(params.collect{|k,v| "#{k}=#{CGI::escape(v.to_s)}"}.join("&"))
    request = Net::HTTP::Post.new(real_url)
    parsed = URI.parse(url)
    net = Net::HTTP.new(parsed.host, parsed.port)
    net.use_ssl = true
    net.verify_mode = OpenSSL::SSL::VERIFY_NONE
    net.set_debug_output $stdout 
    net.read_timeout = 5
    net.open_timeout = 5
    response = net.start do |http|
       http.request(request)
    end
    location = response['location'].scan(/oauth_verifier=(\d*\w*)/)
    oauth_verifier = location[0][0]
end

Whew. Thank you to everyone across the Internet who posted little bits and pieces of this solution. And if you’re ever stuck on an OAuth problem in Ruby, I recommend reading the source of the oauth gem to figure it out. It’s actually quite easy to follow, especially the Consumer class