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/
|