annotate content/Coding/008-oauth-python-gdata.rst @ 8:e29fd75628d6

Turn on disqus comments.
author Brian Neal <bgneal@gmail.com>
date Sat, 01 Feb 2014 13:58:51 -0600
parents 7ce6393e6d30
children
rev   line source
bgneal@4 1 Implementing OAuth using Google's Python Client Library
bgneal@4 2 #######################################################
bgneal@4 3
bgneal@4 4 :date: 2011-07-04 13:00
bgneal@4 5 :tags: Python, OAuth, Google, GData
bgneal@4 6 :slug: implementing-oauth-using-google-s-python-client-library
bgneal@4 7 :author: Brian Neal
bgneal@4 8
bgneal@4 9 My Django_ powered website allows users to submit events for a site calendar
bgneal@4 10 that is built upon Google Calendar. After an admin approves events, I use
bgneal@4 11 Google's `Python Client Library`_ to add, delete, or update events on the Google
bgneal@4 12 calendar associated with my personal Google account. I wrote this application a
bgneal@4 13 few years ago, and it used the ClientLogin_ method for authentication. I
bgneal@4 14 recently decided to upgrade this to the OAuth_ authentication method. The
bgneal@4 15 ClientLogin method isn't very secure and it doesn't play well with Google's
bgneal@4 16 `two-step verification`_. After hearing about a friend who had his GMail account
bgneal@4 17 compromised and all his email deleted I decided it was long past due to get
bgneal@4 18 two-step verification on my account. But first I needed to upgrade my web
bgneal@4 19 application to OAuth.
bgneal@4 20
bgneal@4 21 In this post I'll boil down the code I used to implement the elaborate OAuth
bgneal@4 22 dance. It really isn't that much code, but the Google documentation is somewhat
bgneal@4 23 confusing and scattered across a bewildering number of documents. I found at
bgneal@4 24 least one error in the documentation that I will point out. Although I am using
bgneal@4 25 Django, I will omit details specific to Django where I can.
bgneal@4 26
bgneal@4 27 In addition to switching from ClientLogin to OAuth, I also upgraded to version
bgneal@4 28 2.0 of the Google Data API. This had more implications for my calendar-specific
bgneal@4 29 code, and perhaps I can go over that in a future post.
bgneal@4 30
bgneal@4 31 Getting started and registering with Google
bgneal@4 32 ===========================================
bgneal@4 33
bgneal@4 34 To understand the basics of OAuth, I suggest you read `OAuth 1.0 for Web
bgneal@4 35 Applications`_. I decided to go for maximum security and use RSA-SHA1 signing on
bgneal@4 36 all my requests to Google. This requires that I verify my domain and then
bgneal@4 37 `register my application`_ with Google, which includes uploading a security
bgneal@4 38 certificate. Google provides documentation that describes how you can `create a
bgneal@4 39 self-signing private key and certificate`_ using OpenSSL.
bgneal@4 40
bgneal@4 41 Fetching a Request Token and authorizing access
bgneal@4 42 ===============================================
bgneal@4 43
bgneal@4 44 To perform the first part of the OAuth dance, you must ask Google for a request
bgneal@4 45 token. When you make this request, you state the "scope" of your future work by
bgneal@4 46 listing the Google resources you are going to access. In our case, this is the
bgneal@4 47 calendar resources. You also provide a "consumer key" that Google assigned to
bgneal@4 48 you when you registered your application. This allows Google to retrieve the
bgneal@4 49 security certificate you previously uploaded when you registered. This is very
bgneal@4 50 important because this request is going to be signed with your private key.
bgneal@4 51 Fortunately the Python library takes care of all the signing details, you simply
bgneal@4 52 must provide your private key in PEM format. And finally, you provide a
bgneal@4 53 "callback URL" that Google will send your browser to after you (or your users)
bgneal@4 54 have manually authorized this request.
bgneal@4 55
bgneal@4 56 Once you have received the request token from Google, you have to squirrel it
bgneal@4 57 away somewhere, then redirect your (or your user's) browser to a Google
bgneal@4 58 authorization page. Once the user has authorized your application, Google sends
bgneal@4 59 the browser to the callback URL to continue the process. Here I show the
bgneal@4 60 distilled code I used that asks for a request token, then sends the user to the
bgneal@4 61 authorization page.
bgneal@4 62
bgneal@4 63 .. sourcecode:: python
bgneal@4 64
bgneal@4 65 import gdata.gauth
bgneal@4 66 from gdata.calendar_resource.client import CalendarResourceClient
bgneal@4 67
bgneal@4 68 USER_AGENT = 'mydomain-myapp-v1' # my made up user agent string
bgneal@4 69
bgneal@4 70 client = CalendarResourceClient(None, source=USER_AGENT)
bgneal@4 71
bgneal@4 72 # obtain my private key that I saved previously on the filesystem:
bgneal@4 73 with open(settings.GOOGLE_OAUTH_PRIVATE_KEY_PATH, 'r') as f:
bgneal@4 74 rsa_key = f.read()
bgneal@4 75
bgneal@4 76 # Ask for a request token:
bgneal@4 77 # scopes - a list of scope strings that the request token is for. See
bgneal@4 78 # http://code.google.com/apis/gdata/faq.html#AuthScopes
bgneal@4 79 # callback_url - URL to send the user after authorizing our app
bgneal@4 80
bgneal@4 81 scopes = ['https://www.google.com/calendar/feeds/']
bgneal@4 82 callback_url = 'http://example.com/some/url/to/callback'
bgneal@4 83
bgneal@4 84 request_token = client.GetOAuthToken(
bgneal@4 85 scopes,
bgneal@4 86 callback_url,
bgneal@4 87 settings.GOOGLE_OAUTH_CONSUMER_KEY, # from the registration process
bgneal@4 88 rsa_private_key=rsa_key)
bgneal@4 89
bgneal@4 90 # Before redirecting, save the request token somewhere; here I place it in
bgneal@4 91 # the session (this line is Django specific):
bgneal@4 92 request.session[REQ_TOKEN_SESSION_KEY] = request_token
bgneal@4 93
bgneal@4 94 # Generate the authorization URL.
bgneal@4 95 # Despite the documentation, don't do this:
bgneal@4 96 # auth_url = request_token.generate_authorization_url(domain=None)
bgneal@4 97 # Do this instead if you are not using a Google Apps domain:
bgneal@4 98 auth_url = request_token.generate_authorization_url()
bgneal@4 99
bgneal@4 100 # Now redirect the user somehow to the auth_url; here is how you might do
bgneal@4 101 # it in Django:
bgneal@4 102 return HttpResponseRedirect(auth_url)
bgneal@4 103
bgneal@4 104 A couple of notes on the above:
bgneal@4 105
bgneal@4 106 * You don't have to use ``CalendarResourceClient``, it just made the most sense
bgneal@4 107 for me since I am doing calendar stuff later on. Any class that inherits from
bgneal@4 108 ``gdata.client.GDClient`` will work. You might be able to use that class
bgneal@4 109 directly. Google uses ``gdata.docs.client.DocsClient`` in their examples.
bgneal@4 110 * I chose to store my private key in a file rather than the database. If you do
bgneal@4 111 so, it's probably a good idea to make the file readable only to the user your
bgneal@4 112 webserver runs your application as.
bgneal@4 113 * After getting the request token you must save it somehow. You can save it in
bgneal@4 114 the session, the database, or perhaps a file. Since this is only temporary, I
bgneal@4 115 chose to save it in the session. The code I have here is Django specific.
bgneal@4 116 * When generating the authorization URL, don't pass in ``domain=None`` if you
bgneal@4 117 aren't using a Google Apps domain like the documentation states. This appears
bgneal@4 118 to be an error in the documentation. Just omit it and let it use the default
bgneal@4 119 value of ``"default"`` (see the source code).
bgneal@4 120 * After using the request token to generate the authorization URL, redirect the
bgneal@4 121 browser to it.
bgneal@4 122
bgneal@4 123 Extracting and upgrading to an Access Token
bgneal@4 124 ===========================================
bgneal@4 125
bgneal@4 126 The user will then be taken to a Google authorization page. The page will show the
bgneal@4 127 user what parts of their Google account your application is trying to access
bgneal@4 128 using the information you provided in the ``scopes`` parameter. If the user
bgneal@4 129 accepts, Google will then redirect the browser to your callback URL where we can
bgneal@4 130 complete the process.
bgneal@4 131
bgneal@4 132 The code running at our callback URL must retrieve the request token that we
bgneal@4 133 saved earlier, and combine that with certain ``GET`` parameters Google attached
bgneal@4 134 to our callback URL. This is all done for us by the Python library. We then send
bgneal@4 135 this new token back to Google to upgrade it to an actual access token. If this
bgneal@4 136 succeeds, we can then save this new access token in our database for use in
bgneal@4 137 subsequent Google API operations. The access token is a Python object, so you
bgneal@4 138 can serialize it use the pickle module, or use routines provided by Google
bgneal@4 139 (shown below).
bgneal@4 140
bgneal@4 141 .. sourcecode:: python
bgneal@4 142
bgneal@4 143 # Code running at our callback URL:
bgneal@4 144 # Retrieve the request token we saved earlier in our session
bgneal@4 145 saved_token = request.session[REQ_TOKEN_SESSION_KEY]
bgneal@4 146
bgneal@4 147 # Authorize it by combining it with GET parameters received from Google
bgneal@4 148 request_token = gdata.gauth.AuthorizeRequestToken(saved_token,
bgneal@4 149 request.build_absolute_uri())
bgneal@4 150
bgneal@4 151 # Upgrade it to an access token
bgneal@4 152 client = CalendarResourceClient(None, source=USER_AGENT)
bgneal@4 153 access_token = client.GetAccessToken(request_token)
bgneal@4 154
bgneal@4 155 # Now save access_token somewhere, e.g. a database. So first serialize it:
bgneal@4 156 access_token_str = gdata.gauth.TokenToBlob(access_token)
bgneal@4 157
bgneal@4 158 # Save to database (details omitted)
bgneal@4 159
bgneal@4 160 Some notes on the above code:
bgneal@4 161
bgneal@4 162 * Once called back, our code must retrieve the request token we saved in our
bgneal@4 163 session. The code shown is specific to Django.
bgneal@4 164 * We then combine this saved request token with certain ``GET`` parameters that
bgneal@4 165 Google added to our callback URL. The ``AuthorizeRequestToken`` function takes care of
bgneal@4 166 those details for us. The second argument to that function requires the full URL
bgneal@4 167 including ``GET`` parameters as a string. Here I populate that argument by
bgneal@4 168 using a Django-specific method of retrieving that information.
bgneal@4 169 * Finally, you upgrade your token to an access token by making one last call to
bgneal@4 170 Google. You should now save a serialized version of this access token in your
bgneal@4 171 database for future use.
bgneal@4 172
bgneal@4 173 Using your shiny new Access Token
bgneal@4 174 =================================
bgneal@4 175
bgneal@4 176 Once you have saved your access token, you won't have to do this crazy dance
bgneal@4 177 again until the token either expires, or the user revokes your application's
bgneal@4 178 access to the Google account. To use it in a calendar operation, for example,
bgneal@4 179 you simply retrieve it from your database, deserialize it, and then use it to
bgneal@4 180 create a ``CalendarClient``.
bgneal@4 181
bgneal@4 182 .. sourcecode:: python
bgneal@4 183
bgneal@4 184 from gdata.calendar.client import CalendarClient
bgneal@4 185
bgneal@4 186 # retrieve access token from the database:
bgneal@4 187 access_token_str = ...
bgneal@4 188 access_token = gdata.gauth.TokenFromBlob(access_token_str)
bgneal@4 189
bgneal@4 190 client = CalendarClient(source=USER_AGENT, auth_token=access_token)
bgneal@4 191
bgneal@4 192 # now use client to make calendar operations...
bgneal@4 193
bgneal@4 194 Conclusion
bgneal@4 195 ==========
bgneal@4 196
bgneal@4 197 The main reason I wrote this blog post is I wanted to show a concrete example of
bgneal@4 198 using RSA-SHA1 and version 2.0 of the Google API together. All of the
bgneal@4 199 information I have presented is in the Google documentation, but it is spread
bgneal@4 200 across several documents and jumbled up with example code for version 1.0 and
bgneal@4 201 HMAC-SHA1. Do not be afraid to look at the source code for the Python client
bgneal@4 202 library. Despite Google's strange habit of ignoring PEP-8_ and using
bgneal@4 203 LongJavaLikeMethodNames, the code is logical and easy to read. Their library is
bgneal@4 204 built up in layers, and you may have to dip down a few levels to find out what
bgneal@4 205 is going on, but it is fairly straightforward to read if you combine it with
bgneal@4 206 their online documentation.
bgneal@4 207
bgneal@4 208 I hope someone finds this useful. Your feedback is welcome.
bgneal@4 209
bgneal@4 210
bgneal@4 211 .. _Django: http://djangoproject.com
bgneal@4 212 .. _Python Client Library: http://code.google.com/apis/calendar/data/2.0/developers_guide_python.html
bgneal@4 213 .. _ClientLogin: http://code.google.com/apis/calendar/data/2.0/developers_guide_python.html#AuthClientLogin
bgneal@4 214 .. _OAuth: http://code.google.com/apis/gdata/docs/auth/oauth.html
bgneal@4 215 .. _two-step verification: http://googleblog.blogspot.com/2011/02/advanced-sign-in-security-for-your.html
bgneal@4 216 .. _OAuth 1.0 for Web Applications: http://code.google.com/apis/accounts/docs/OAuth.html
bgneal@4 217 .. _register my application: http://code.google.com/apis/accounts/docs/RegistrationForWebAppsAuto.html
bgneal@4 218 .. _create a self-signing private key and certificate: http://code.google.com/apis/gdata/docs/auth/oauth.html#GeneratingKeyCert
bgneal@4 219 .. _PEP-8: http://www.python.org/dev/peps/pep-0008/