bgneal@4: Implementing OAuth using Google's Python Client Library bgneal@4: ####################################################### bgneal@4: bgneal@4: :date: 2011-07-04 13:00 bgneal@4: :tags: Python, OAuth, Google, GData bgneal@4: :slug: implementing-oauth-using-google-s-python-client-library bgneal@4: :author: Brian Neal bgneal@4: bgneal@4: My Django_ powered website allows users to submit events for a site calendar bgneal@4: that is built upon Google Calendar. After an admin approves events, I use bgneal@4: Google's `Python Client Library`_ to add, delete, or update events on the Google bgneal@4: calendar associated with my personal Google account. I wrote this application a bgneal@4: few years ago, and it used the ClientLogin_ method for authentication. I bgneal@4: recently decided to upgrade this to the OAuth_ authentication method. The bgneal@4: ClientLogin method isn't very secure and it doesn't play well with Google's bgneal@4: `two-step verification`_. After hearing about a friend who had his GMail account bgneal@4: compromised and all his email deleted I decided it was long past due to get bgneal@4: two-step verification on my account. But first I needed to upgrade my web bgneal@4: application to OAuth. bgneal@4: bgneal@4: In this post I'll boil down the code I used to implement the elaborate OAuth bgneal@4: dance. It really isn't that much code, but the Google documentation is somewhat bgneal@4: confusing and scattered across a bewildering number of documents. I found at bgneal@4: least one error in the documentation that I will point out. Although I am using bgneal@4: Django, I will omit details specific to Django where I can. bgneal@4: bgneal@4: In addition to switching from ClientLogin to OAuth, I also upgraded to version bgneal@4: 2.0 of the Google Data API. This had more implications for my calendar-specific bgneal@4: code, and perhaps I can go over that in a future post. bgneal@4: bgneal@4: Getting started and registering with Google bgneal@4: =========================================== bgneal@4: bgneal@4: To understand the basics of OAuth, I suggest you read `OAuth 1.0 for Web bgneal@4: Applications`_. I decided to go for maximum security and use RSA-SHA1 signing on bgneal@4: all my requests to Google. This requires that I verify my domain and then bgneal@4: `register my application`_ with Google, which includes uploading a security bgneal@4: certificate. Google provides documentation that describes how you can `create a bgneal@4: self-signing private key and certificate`_ using OpenSSL. bgneal@4: bgneal@4: Fetching a Request Token and authorizing access bgneal@4: =============================================== bgneal@4: bgneal@4: To perform the first part of the OAuth dance, you must ask Google for a request bgneal@4: token. When you make this request, you state the "scope" of your future work by bgneal@4: listing the Google resources you are going to access. In our case, this is the bgneal@4: calendar resources. You also provide a "consumer key" that Google assigned to bgneal@4: you when you registered your application. This allows Google to retrieve the bgneal@4: security certificate you previously uploaded when you registered. This is very bgneal@4: important because this request is going to be signed with your private key. bgneal@4: Fortunately the Python library takes care of all the signing details, you simply bgneal@4: must provide your private key in PEM format. And finally, you provide a bgneal@4: "callback URL" that Google will send your browser to after you (or your users) bgneal@4: have manually authorized this request. bgneal@4: bgneal@4: Once you have received the request token from Google, you have to squirrel it bgneal@4: away somewhere, then redirect your (or your user's) browser to a Google bgneal@4: authorization page. Once the user has authorized your application, Google sends bgneal@4: the browser to the callback URL to continue the process. Here I show the bgneal@4: distilled code I used that asks for a request token, then sends the user to the bgneal@4: authorization page. bgneal@4: bgneal@4: .. sourcecode:: python bgneal@4: bgneal@4: import gdata.gauth bgneal@4: from gdata.calendar_resource.client import CalendarResourceClient bgneal@4: bgneal@4: USER_AGENT = 'mydomain-myapp-v1' # my made up user agent string bgneal@4: bgneal@4: client = CalendarResourceClient(None, source=USER_AGENT) bgneal@4: bgneal@4: # obtain my private key that I saved previously on the filesystem: bgneal@4: with open(settings.GOOGLE_OAUTH_PRIVATE_KEY_PATH, 'r') as f: bgneal@4: rsa_key = f.read() bgneal@4: bgneal@4: # Ask for a request token: bgneal@4: # scopes - a list of scope strings that the request token is for. See bgneal@4: # http://code.google.com/apis/gdata/faq.html#AuthScopes bgneal@4: # callback_url - URL to send the user after authorizing our app bgneal@4: bgneal@4: scopes = ['https://www.google.com/calendar/feeds/'] bgneal@4: callback_url = 'http://example.com/some/url/to/callback' bgneal@4: bgneal@4: request_token = client.GetOAuthToken( bgneal@4: scopes, bgneal@4: callback_url, bgneal@4: settings.GOOGLE_OAUTH_CONSUMER_KEY, # from the registration process bgneal@4: rsa_private_key=rsa_key) bgneal@4: bgneal@4: # Before redirecting, save the request token somewhere; here I place it in bgneal@4: # the session (this line is Django specific): bgneal@4: request.session[REQ_TOKEN_SESSION_KEY] = request_token bgneal@4: bgneal@4: # Generate the authorization URL. bgneal@4: # Despite the documentation, don't do this: bgneal@4: # auth_url = request_token.generate_authorization_url(domain=None) bgneal@4: # Do this instead if you are not using a Google Apps domain: bgneal@4: auth_url = request_token.generate_authorization_url() bgneal@4: bgneal@4: # Now redirect the user somehow to the auth_url; here is how you might do bgneal@4: # it in Django: bgneal@4: return HttpResponseRedirect(auth_url) bgneal@4: bgneal@4: A couple of notes on the above: bgneal@4: bgneal@4: * You don't have to use ``CalendarResourceClient``, it just made the most sense bgneal@4: for me since I am doing calendar stuff later on. Any class that inherits from bgneal@4: ``gdata.client.GDClient`` will work. You might be able to use that class bgneal@4: directly. Google uses ``gdata.docs.client.DocsClient`` in their examples. bgneal@4: * I chose to store my private key in a file rather than the database. If you do bgneal@4: so, it's probably a good idea to make the file readable only to the user your bgneal@4: webserver runs your application as. bgneal@4: * After getting the request token you must save it somehow. You can save it in bgneal@4: the session, the database, or perhaps a file. Since this is only temporary, I bgneal@4: chose to save it in the session. The code I have here is Django specific. bgneal@4: * When generating the authorization URL, don't pass in ``domain=None`` if you bgneal@4: aren't using a Google Apps domain like the documentation states. This appears bgneal@4: to be an error in the documentation. Just omit it and let it use the default bgneal@4: value of ``"default"`` (see the source code). bgneal@4: * After using the request token to generate the authorization URL, redirect the bgneal@4: browser to it. bgneal@4: bgneal@4: Extracting and upgrading to an Access Token bgneal@4: =========================================== bgneal@4: bgneal@4: The user will then be taken to a Google authorization page. The page will show the bgneal@4: user what parts of their Google account your application is trying to access bgneal@4: using the information you provided in the ``scopes`` parameter. If the user bgneal@4: accepts, Google will then redirect the browser to your callback URL where we can bgneal@4: complete the process. bgneal@4: bgneal@4: The code running at our callback URL must retrieve the request token that we bgneal@4: saved earlier, and combine that with certain ``GET`` parameters Google attached bgneal@4: to our callback URL. This is all done for us by the Python library. We then send bgneal@4: this new token back to Google to upgrade it to an actual access token. If this bgneal@4: succeeds, we can then save this new access token in our database for use in bgneal@4: subsequent Google API operations. The access token is a Python object, so you bgneal@4: can serialize it use the pickle module, or use routines provided by Google bgneal@4: (shown below). bgneal@4: bgneal@4: .. sourcecode:: python bgneal@4: bgneal@4: # Code running at our callback URL: bgneal@4: # Retrieve the request token we saved earlier in our session bgneal@4: saved_token = request.session[REQ_TOKEN_SESSION_KEY] bgneal@4: bgneal@4: # Authorize it by combining it with GET parameters received from Google bgneal@4: request_token = gdata.gauth.AuthorizeRequestToken(saved_token, bgneal@4: request.build_absolute_uri()) bgneal@4: bgneal@4: # Upgrade it to an access token bgneal@4: client = CalendarResourceClient(None, source=USER_AGENT) bgneal@4: access_token = client.GetAccessToken(request_token) bgneal@4: bgneal@4: # Now save access_token somewhere, e.g. a database. So first serialize it: bgneal@4: access_token_str = gdata.gauth.TokenToBlob(access_token) bgneal@4: bgneal@4: # Save to database (details omitted) bgneal@4: bgneal@4: Some notes on the above code: bgneal@4: bgneal@4: * Once called back, our code must retrieve the request token we saved in our bgneal@4: session. The code shown is specific to Django. bgneal@4: * We then combine this saved request token with certain ``GET`` parameters that bgneal@4: Google added to our callback URL. The ``AuthorizeRequestToken`` function takes care of bgneal@4: those details for us. The second argument to that function requires the full URL bgneal@4: including ``GET`` parameters as a string. Here I populate that argument by bgneal@4: using a Django-specific method of retrieving that information. bgneal@4: * Finally, you upgrade your token to an access token by making one last call to bgneal@4: Google. You should now save a serialized version of this access token in your bgneal@4: database for future use. bgneal@4: bgneal@4: Using your shiny new Access Token bgneal@4: ================================= bgneal@4: bgneal@4: Once you have saved your access token, you won't have to do this crazy dance bgneal@4: again until the token either expires, or the user revokes your application's bgneal@4: access to the Google account. To use it in a calendar operation, for example, bgneal@4: you simply retrieve it from your database, deserialize it, and then use it to bgneal@4: create a ``CalendarClient``. bgneal@4: bgneal@4: .. sourcecode:: python bgneal@4: bgneal@4: from gdata.calendar.client import CalendarClient bgneal@4: bgneal@4: # retrieve access token from the database: bgneal@4: access_token_str = ... bgneal@4: access_token = gdata.gauth.TokenFromBlob(access_token_str) bgneal@4: bgneal@4: client = CalendarClient(source=USER_AGENT, auth_token=access_token) bgneal@4: bgneal@4: # now use client to make calendar operations... bgneal@4: bgneal@4: Conclusion bgneal@4: ========== bgneal@4: bgneal@4: The main reason I wrote this blog post is I wanted to show a concrete example of bgneal@4: using RSA-SHA1 and version 2.0 of the Google API together. All of the bgneal@4: information I have presented is in the Google documentation, but it is spread bgneal@4: across several documents and jumbled up with example code for version 1.0 and bgneal@4: HMAC-SHA1. Do not be afraid to look at the source code for the Python client bgneal@4: library. Despite Google's strange habit of ignoring PEP-8_ and using bgneal@4: LongJavaLikeMethodNames, the code is logical and easy to read. Their library is bgneal@4: built up in layers, and you may have to dip down a few levels to find out what bgneal@4: is going on, but it is fairly straightforward to read if you combine it with bgneal@4: their online documentation. bgneal@4: bgneal@4: I hope someone finds this useful. Your feedback is welcome. bgneal@4: bgneal@4: bgneal@4: .. _Django: http://djangoproject.com bgneal@4: .. _Python Client Library: http://code.google.com/apis/calendar/data/2.0/developers_guide_python.html bgneal@4: .. _ClientLogin: http://code.google.com/apis/calendar/data/2.0/developers_guide_python.html#AuthClientLogin bgneal@4: .. _OAuth: http://code.google.com/apis/gdata/docs/auth/oauth.html bgneal@4: .. _two-step verification: http://googleblog.blogspot.com/2011/02/advanced-sign-in-security-for-your.html bgneal@4: .. _OAuth 1.0 for Web Applications: http://code.google.com/apis/accounts/docs/OAuth.html bgneal@4: .. _register my application: http://code.google.com/apis/accounts/docs/RegistrationForWebAppsAuto.html bgneal@4: .. _create a self-signing private key and certificate: http://code.google.com/apis/gdata/docs/auth/oauth.html#GeneratingKeyCert bgneal@4: .. _PEP-8: http://www.python.org/dev/peps/pep-0008/