OpenID and oAuth on App Engine
May 11th, 2009Building on my previous post, here are some things I learned today while working to get OpenID and oAuth playing nicely with Django on App Engine.
While App Engine has a very nice login system that hooks seamlessly into Google Accounts, gaining access to the user’s data via one of the Gdata feeds requires an additional authorization (you would use oAuth of course). However, Google has also introduced a federated login method whereby you can send the user to one screen where they both login and approve your access via oAuth to their data. So, I gave up development speed in favor of something that is hopefully simpler for my users. If you want to know more, Joseph Smarr of Plaxo has a good writeup of how the hybrid OpenID + oAuth combination came about.
After many frustrating attempts trying to get any of the many Django OpenID apps to work, all of which seemed to go haywire when touching App Engine (even if they claimed support), I found the google-app-engine-django-openid app. It’s quite simplistic – it doesn’t seem to connect to Django’s auth system at all – but, most importantly, it worked right out of the box on my App Engine install. Thanks, Wesley! Since I am requiring all users to login via a Google account, I simply skipped the first step in google-app-engine-django-openid’s login process, having the user enter their OpenID URL, and went to the next step, endpoint discovery. Beyond that all I had to do was add the additional oAuth parameters to my OpenID request and, on the return, upgrade the signed request token to an access token. gdata-python-client provides all the oAuth mechanisms you need as part of its authorization suite.
Of course, getting an access token was easier said than done, due the gdata library’s oAuth module being designed with the normal oAuth authorization flow in mind. To start using it much later in the oAuth process required me to initialize a bunch of things I wouldn’t have otherwise needed to handle. In the interest in saving people time, here’s the final code:
signed_request_token = gdata.auth.OAuthToken(key=args['openid.ext2.request_token'], secret="") gdata_service = gdata.service.GDataService() gdata.alt.appengine.run_on_appengine(gdata_service) gdata_service.SetOAuthInputParameters(gdata.auth.OAuthSignatureMethod.HMAC_SHA1, settings.GOOGLE_CONSUMER_KEY, settings.GOOGLE_CONSUMER_SECRET) access_token = gdata_service.UpgradeToOAuthAccessToken(signed_request_token) person.accessToken = str(access_token)
It’s not a lot of code, really, but it took a lot of poking through the gdata code to figure it all out! Note that the access token returned by UpgradeToOAuthAccessToken() is not a string but can easily be converted to and from a string for storage.
When using parts of the gdata-python-client library, always make sure to set the http transport to App Engine’s urllib: gdata.service.http_request_handler = gdata.urlfetch. In general, if you’re getting any sort of weird error from something that uses a network connection, it’s probably because you’re not using Google’s URL fetch service.
In additional, one key thing I learned is that, while you don’t get a secret back with your signed request token, you must pass along an empty string for the secret when calling gdata.service.GDataService.UpgradeToOAuthAccessToken. None or False will cause errors in gdata’s oAuth library.
Finally, testing. Because Google requires your realm to match your domain for the oAuth request, I couldn’t test the authentication and authorization process on my local machine running the App Engine SDK behind a router. Instead, I just keep pushed changes to my live install on App Engine. I’m sure there’s a better solution, most likely running the SDK on a machine with a public address and getting an oAuth consumer key from Google tied to that address. Also, I didn’t use it but the oAuth Playground looks like a good place to test your oAuth interaction with Gdata services.
May 24th, 2009 at 4:45 pm
[...] my previous post I described how to use Google’s federated login to get an oAuth access token. Now that we’ve stored our access token, we’re going to [...]
August 11th, 2009 at 7:54 am
I’m trying to do the same thing.
Would you please send me a copy of where exactly the final code goes?
The change you’ve made in openidgae as well as the token management.
August 11th, 2009 at 10:50 am
How did you get args['openid.ext2.request_token']?
August 11th, 2009 at 5:59 pm
The args dictionary is created with:
args = openidgae.views.args_to_dict(request.GET)request.GETis defined by Django. This is in mylogin_succeededcontroller.August 12th, 2009 at 3:08 am
I guess the problem is the “add the additional oAuth parameters to my OpenID request” section, what exactly did you do?
BTW, what I’ve done is put the following code before the return statement of openidgae.views.OpenIDFinish:
request_token = args['openid.ext2.request_token']
signed_request_token = gdata.auth.OAuthToken(key=request_token, secret=”")
gdata_service = gdata.service.GDataService()
gdata.service.http_request_handler = gdata.urlfetch
from gdata.alt.appengine import run_on_appengine
run_on_appengine(gdata_service)
gdata_service.SetOAuthInputParameters(gdata.auth.OAuthSignatureMethod.HMAC_SHA1, settings.GOOGLE_CONSUMER_KEY, settings.GOOGLE_CONSUMER_SECRET)
access_token = gdata_service.UpgradeToOAuthAccessToken(signed_request_token)
person.accessToken = str(access_token)
……
August 14th, 2009 at 9:03 pm
Yucc, I put my file here: http://gist.github.com/168067
Hope that helps.
August 20th, 2009 at 5:59 am
Really helps!
If I could ask what did you do by ‘openidgae.views.initOpenId()’?
August 20th, 2009 at 7:40 am
It’s the from the openidgae library and it needed to set everything up.
August 24th, 2009 at 4:25 am
There’s no such method any more in the openidgae library, so I just remove such a set-up, hope things can work out for me.