Because I spend so much time working with Google technology I tend to use the Google APIs Python Client more often than any other external API client. As such I've gotten very used to using Google's
oauth2client. I personally think it's one of the best (if not the most complete) implementations of OAuth2 for python.
However, I do not exclusively work in the Google world and every now and again I have to interact with the Salesforce API. Both Google and Salesforce support (for varying definitions of it) OAuth2 to access their libraries. So it should be pretty trivial to use oauth2client to handle the authorization flow for Salesforce, right? Sadly, it's not quite that simple.
I spent most of my morning today hacking around Salesforce's OAuth2 implementation. If you just want to get it working then go ahead and open up the project page for python-salesforce-oauth2. The rest of this post is what lengths I had to go to in order to get it to work.
My first attempt was simply to manually specify the OAuth2 URLs and see what would happen:
flow = OAuth2WebServerFlow( client_id=sf_settings['client_id'], client_secret=sf_settings['client_secret'], scope=['api', 'refresh_token', 'full'], redirect_uri=self.uri(action='oauth2callback', _full=True), auth_uri="https://na15.salesforce.com/services/oauth2/authorize", token_uri="https://na15.salesforce.com/services/oauth2/token" ) return self.redirect(flow.step1_get_authorize_url())
Luckily enough this worked. It properly redirected to the Salesforce authorization url and I was able to approve the app. Cool. The next step was to write the callback handler. Again, let's try to do it the normal way:
code = self.request.params.get('code') flow = self._create_flow() credentials = flow.step2_exchange(code)
This is where I ran into my first issue. Salesforce promptly returned an invalid request error. The error informed me that exchange step doesn't support specifying the scope parameter (although the first step requires it). It seems odd that Salesforce couldn't just ignore the parameter but instead had to throw an error thus making it difficult for us. I ended up having to sub-class OAuth2WebServerFlow and creating my own step2_exchange function that simply didn't include the scopes parameter for that step.
After creating that class I was able to successfully get credentials. The next logical thing to do is to make a request and see if it works.
credentials = self.session['sf_credentials'] http = httplib2.Http() credentials.authorize(http) r, c = http.request("https://na13.salesforce.com/services/data/v29.0/sobjects")
Nope. I got an excruciating strange error from deep in httplib2 traced back to the credentials refresh routine. It seems that httplib2 choked on trying parse the headers in Salesforce's 401 response. For those who aren't aware using
credentails.authorize allows the credentials to catch a 401 response, refresh the access token, and retry the request. This is the expected thing to happen when you make the first request after obtaining credentials. Because httplib2 is throwing a
MalformedHeaderException credentials can't catch it and refresh properly. After a fair amount of research I came across these two issues (1 & 2). Awesome. I adapted the patch in the second one into a monkey-patch so I didn't have to change httplib2 itself.
Finally. It all works. I packaged up everything into a small library.