blob: 136adcdc1b256a4a43434a0a5fb488d4795b4cb1 [file] [log] [blame]
Joe Gregorio20a5aa92011-04-01 17:44:25 -04001# Copyright (C) 2010 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
Joe Gregorio67d77772010-09-01 16:45:45 -040014
15"""Utilities for OAuth.
16
17Utilities for making it easier to work with OAuth.
18"""
19
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
21
Joe Gregorioa0a52e42011-02-17 17:13:26 -050022
Joe Gregorio67d77772010-09-01 16:45:45 -040023import copy
Joe Gregorio845a5452010-09-08 13:50:34 -040024import httplib2
Joe Gregorio695fdc12011-01-16 16:46:55 -050025import logging
Joe Gregorio67d77772010-09-01 16:45:45 -040026import oauth2 as oauth
Joe Gregorio845a5452010-09-08 13:50:34 -040027import urllib
Joe Gregorio695fdc12011-01-16 16:46:55 -050028import urlparse
Joe Gregorio549230c2012-01-11 10:38:05 -050029
30from oauth2client.anyjson import simplejson
Joe Gregorioe6ee3e52012-02-01 18:38:54 -080031from oauth2client.client import Credentials
32from oauth2client.client import Flow
33from oauth2client.client import Storage
Joe Gregorio67d77772010-09-01 16:45:45 -040034
35try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -050036 from urlparse import parse_qsl
Joe Gregorio67d77772010-09-01 16:45:45 -040037except ImportError:
Joe Gregorio7c22ab22011-02-16 15:32:39 -050038 from cgi import parse_qsl
Joe Gregorio845a5452010-09-08 13:50:34 -040039
40
Tom Miller05cd4f52010-10-06 11:09:12 -070041class Error(Exception):
42 """Base error for this module."""
43 pass
44
45
46class RequestError(Error):
47 """Error occurred during request."""
48 pass
49
50
51class MissingParameter(Error):
Joe Gregorio67d77772010-09-01 16:45:45 -040052 pass
53
Joe Gregorio845a5452010-09-08 13:50:34 -040054
Joe Gregorioa0a52e42011-02-17 17:13:26 -050055class CredentialsInvalidError(Error):
56 pass
57
58
Joe Gregorio845a5452010-09-08 13:50:34 -040059def _abstract():
Joe Gregorio7943c5d2010-09-08 16:11:43 -040060 raise NotImplementedError('You need to override this function')
Joe Gregorio67d77772010-09-01 16:45:45 -040061
62
Joe Gregorio67d77772010-09-01 16:45:45 -040063def _oauth_uri(name, discovery, params):
ade@google.coma9907a22010-12-11 02:44:37 +000064 """Look up the OAuth URI from the discovery
Joe Gregorio845a5452010-09-08 13:50:34 -040065 document and add query parameters based on
66 params.
67
68 name - The name of the OAuth URI to lookup, one
69 of 'request', 'access', or 'authorize'.
70 discovery - Portion of discovery document the describes
71 the OAuth endpoints.
72 params - Dictionary that is used to form the query parameters
73 for the specified URI.
74 """
Joe Gregorio67d77772010-09-01 16:45:45 -040075 if name not in ['request', 'access', 'authorize']:
76 raise KeyError(name)
Joe Gregorio7943c5d2010-09-08 16:11:43 -040077 keys = discovery[name]['parameters'].keys()
Joe Gregorio67d77772010-09-01 16:45:45 -040078 query = {}
79 for key in keys:
80 if key in params:
81 query[key] = params[key]
82 return discovery[name]['url'] + '?' + urllib.urlencode(query)
83
Joe Gregorio845a5452010-09-08 13:50:34 -040084
Joe Gregorioa0a52e42011-02-17 17:13:26 -050085
Joe Gregorio845a5452010-09-08 13:50:34 -040086class OAuthCredentials(Credentials):
87 """Credentials object for OAuth 1.0a
88 """
89
90 def __init__(self, consumer, token, user_agent):
91 """
92 consumer - An instance of oauth.Consumer.
93 token - An instance of oauth.Token constructed with
94 the access token and secret.
95 user_agent - The HTTP User-Agent to provide for this application.
96 """
97 self.consumer = consumer
98 self.token = token
99 self.user_agent = user_agent
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500100 self.store = None
101
102 # True if the credentials have been revoked
103 self._invalid = False
104
105 @property
106 def invalid(self):
107 """True if the credentials are invalid, such as being revoked."""
108 return getattr(self, "_invalid", False)
109
110 def set_store(self, store):
111 """Set the storage for the credential.
112
113 Args:
114 store: callable, a callable that when passed a Credential
115 will store the credential back to where it came from.
116 This is needed to store the latest access_token if it
117 has been revoked.
118 """
119 self.store = store
120
121 def __getstate__(self):
Joe Gregorio825d78d2011-02-18 15:07:17 -0500122 """Trim the state down to something that can be pickled."""
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500123 d = copy.copy(self.__dict__)
124 del d['store']
125 return d
126
127 def __setstate__(self, state):
Joe Gregorio825d78d2011-02-18 15:07:17 -0500128 """Reconstitute the state of the object from being pickled."""
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500129 self.__dict__.update(state)
130 self.store = None
Joe Gregorio845a5452010-09-08 13:50:34 -0400131
132 def authorize(self, http):
Joe Gregorio0bc70912011-05-24 15:30:49 -0400133 """Authorize an httplib2.Http instance with these Credentials
134
Joe Gregorio845a5452010-09-08 13:50:34 -0400135 Args:
136 http - An instance of httplib2.Http
137 or something that acts like it.
138
139 Returns:
140 A modified instance of http that was passed in.
141
142 Example:
143
144 h = httplib2.Http()
145 h = credentials.authorize(h)
146
147 You can't create a new OAuth
148 subclass of httplib2.Authenication because
149 it never gets passed the absolute URI, which is
150 needed for signing. So instead we have to overload
151 'request' with a closure that adds in the
152 Authorization header and then calls the original version
153 of 'request()'.
154 """
155 request_orig = http.request
156 signer = oauth.SignatureMethod_HMAC_SHA1()
157
158 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400159 def new_request(uri, method='GET', body=None, headers=None,
Joe Gregorio845a5452010-09-08 13:50:34 -0400160 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
161 connection_type=None):
162 """Modify the request headers to add the appropriate
163 Authorization header."""
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500164 response_code = 302
165 http.follow_redirects = False
166 while response_code in [301, 302]:
167 req = oauth.Request.from_consumer_and_token(
168 self.consumer, self.token, http_method=method, http_url=uri)
169 req.sign_request(signer, self.consumer, self.token)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500170 if headers is None:
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500171 headers = {}
172 headers.update(req.to_header())
173 if 'user-agent' in headers:
174 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
175 else:
176 headers['user-agent'] = self.user_agent
Joe Gregorio0bc70912011-05-24 15:30:49 -0400177
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500178 resp, content = request_orig(uri, method, body, headers,
179 redirections, connection_type)
180 response_code = resp.status
181 if response_code in [301, 302]:
182 uri = resp['location']
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500183
184 # Update the stored credential if it becomes invalid.
185 if response_code == 401:
186 logging.info('Access token no longer valid: %s' % content)
187 self._invalid = True
188 if self.store is not None:
189 self.store(self)
190 raise CredentialsInvalidError("Credentials are no longer valid.")
191
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500192 return resp, content
Joe Gregorio845a5452010-09-08 13:50:34 -0400193
194 http.request = new_request
195 return http
196
197
Joe Gregorio0bc70912011-05-24 15:30:49 -0400198class TwoLeggedOAuthCredentials(Credentials):
199 """Two Legged Credentials object for OAuth 1.0a.
200
201 The Two Legged object is created directly, not from a flow. Once you
202 authorize and httplib2.Http instance you can change the requestor and that
203 change will propogate to the authorized httplib2.Http instance. For example:
204
205 http = httplib2.Http()
206 http = credentials.authorize(http)
207
208 credentials.requestor = 'foo@example.info'
209 http.request(...)
210 credentials.requestor = 'bar@example.info'
211 http.request(...)
212 """
213
214 def __init__(self, consumer_key, consumer_secret, user_agent):
215 """
216 Args:
217 consumer_key: string, An OAuth 1.0 consumer key
218 consumer_secret: string, An OAuth 1.0 consumer secret
219 user_agent: string, The HTTP User-Agent to provide for this application.
220 """
221 self.consumer = oauth.Consumer(consumer_key, consumer_secret)
222 self.user_agent = user_agent
223 self.store = None
224
225 # email address of the user to act on the behalf of.
226 self._requestor = None
227
228 @property
229 def invalid(self):
230 """True if the credentials are invalid, such as being revoked.
231
232 Always returns False for Two Legged Credentials.
233 """
234 return False
235
236 def getrequestor(self):
237 return self._requestor
238
239 def setrequestor(self, email):
240 self._requestor = email
241
242 requestor = property(getrequestor, setrequestor, None,
243 'The email address of the user to act on behalf of')
244
245 def set_store(self, store):
246 """Set the storage for the credential.
247
248 Args:
249 store: callable, a callable that when passed a Credential
250 will store the credential back to where it came from.
251 This is needed to store the latest access_token if it
252 has been revoked.
253 """
254 self.store = store
255
256 def __getstate__(self):
257 """Trim the state down to something that can be pickled."""
258 d = copy.copy(self.__dict__)
259 del d['store']
260 return d
261
262 def __setstate__(self, state):
263 """Reconstitute the state of the object from being pickled."""
264 self.__dict__.update(state)
265 self.store = None
266
267 def authorize(self, http):
268 """Authorize an httplib2.Http instance with these Credentials
269
270 Args:
271 http - An instance of httplib2.Http
272 or something that acts like it.
273
274 Returns:
275 A modified instance of http that was passed in.
276
277 Example:
278
279 h = httplib2.Http()
280 h = credentials.authorize(h)
281
282 You can't create a new OAuth
283 subclass of httplib2.Authenication because
284 it never gets passed the absolute URI, which is
285 needed for signing. So instead we have to overload
286 'request' with a closure that adds in the
287 Authorization header and then calls the original version
288 of 'request()'.
289 """
290 request_orig = http.request
291 signer = oauth.SignatureMethod_HMAC_SHA1()
292
293 # The closure that will replace 'httplib2.Http.request'.
294 def new_request(uri, method='GET', body=None, headers=None,
295 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
296 connection_type=None):
297 """Modify the request headers to add the appropriate
298 Authorization header."""
299 response_code = 302
300 http.follow_redirects = False
301 while response_code in [301, 302]:
302 # add in xoauth_requestor_id=self._requestor to the uri
303 if self._requestor is None:
304 raise MissingParameter(
305 'Requestor must be set before using TwoLeggedOAuthCredentials')
306 parsed = list(urlparse.urlparse(uri))
307 q = parse_qsl(parsed[4])
308 q.append(('xoauth_requestor_id', self._requestor))
309 parsed[4] = urllib.urlencode(q)
310 uri = urlparse.urlunparse(parsed)
311
312 req = oauth.Request.from_consumer_and_token(
313 self.consumer, None, http_method=method, http_url=uri)
314 req.sign_request(signer, self.consumer, None)
315 if headers is None:
316 headers = {}
317 headers.update(req.to_header())
318 if 'user-agent' in headers:
319 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
320 else:
321 headers['user-agent'] = self.user_agent
322 resp, content = request_orig(uri, method, body, headers,
323 redirections, connection_type)
324 response_code = resp.status
325 if response_code in [301, 302]:
326 uri = resp['location']
327
328 if response_code == 401:
329 logging.info('Access token no longer valid: %s' % content)
330 # Do not store the invalid state of the Credentials because
331 # being 2LO they could be reinstated in the future.
332 raise CredentialsInvalidError("Credentials are invalid.")
333
334 return resp, content
335
336 http.request = new_request
337 return http
338
339
Joe Gregorio695fdc12011-01-16 16:46:55 -0500340class FlowThreeLegged(Flow):
Joe Gregorio845a5452010-09-08 13:50:34 -0400341 """Does the Three Legged Dance for OAuth 1.0a.
342 """
343
Joe Gregorio67d77772010-09-01 16:45:45 -0400344 def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
345 **kwargs):
Joe Gregorio845a5452010-09-08 13:50:34 -0400346 """
347 discovery - Section of the API discovery document that describes
348 the OAuth endpoints.
349 consumer_key - OAuth consumer key
350 consumer_secret - OAuth consumer secret
351 user_agent - The HTTP User-Agent that identifies the application.
352 **kwargs - The keyword arguments are all optional and required
353 parameters for the OAuth calls.
354 """
Joe Gregorio67d77772010-09-01 16:45:45 -0400355 self.discovery = discovery
356 self.consumer_key = consumer_key
357 self.consumer_secret = consumer_secret
358 self.user_agent = user_agent
359 self.params = kwargs
360 self.request_token = {}
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400361 required = {}
362 for uriinfo in discovery.itervalues():
363 for name, value in uriinfo['parameters'].iteritems():
364 if value['required'] and not name.startswith('oauth_'):
365 required[name] = 1
366 for key in required.iterkeys():
Joe Gregorio67d77772010-09-01 16:45:45 -0400367 if key not in self.params:
368 raise MissingParameter('Required parameter %s not supplied' % key)
369
Joe Gregorio845a5452010-09-08 13:50:34 -0400370 def step1_get_authorize_url(self, oauth_callback='oob'):
Joe Gregorio67d77772010-09-01 16:45:45 -0400371 """Returns a URI to redirect to the provider.
372
Joe Gregorio845a5452010-09-08 13:50:34 -0400373 oauth_callback - Either the string 'oob' for a non-web-based application,
374 or a URI that handles the callback from the authorization
375 server.
376
377 If oauth_callback is 'oob' then pass in the
378 generated verification code to step2_exchange,
379 otherwise pass in the query parameters received
380 at the callback uri to step2_exchange.
Joe Gregorio67d77772010-09-01 16:45:45 -0400381 """
382 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
383 client = oauth.Client(consumer)
384
385 headers = {
386 'user-agent': self.user_agent,
387 'content-type': 'application/x-www-form-urlencoded'
388 }
389 body = urllib.urlencode({'oauth_callback': oauth_callback})
390 uri = _oauth_uri('request', self.discovery, self.params)
Joe Gregorio845a5452010-09-08 13:50:34 -0400391
Joe Gregorio67d77772010-09-01 16:45:45 -0400392 resp, content = client.request(uri, 'POST', headers=headers,
393 body=body)
394 if resp['status'] != '200':
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500395 logging.error('Failed to retrieve temporary authorization: %s', content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700396 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400397
398 self.request_token = dict(parse_qsl(content))
399
400 auth_params = copy.copy(self.params)
401 auth_params['oauth_token'] = self.request_token['oauth_token']
402
Joe Gregorio845a5452010-09-08 13:50:34 -0400403 return _oauth_uri('authorize', self.discovery, auth_params)
Joe Gregorio67d77772010-09-01 16:45:45 -0400404
Joe Gregorio845a5452010-09-08 13:50:34 -0400405 def step2_exchange(self, verifier):
406 """Exhanges an authorized request token
407 for OAuthCredentials.
Joe Gregorio67d77772010-09-01 16:45:45 -0400408
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500409 Args:
410 verifier: string, dict - either the verifier token, or a dictionary
Joe Gregorio845a5452010-09-08 13:50:34 -0400411 of the query parameters to the callback, which contains
412 the oauth_verifier.
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500413 Returns:
414 The Credentials object.
Joe Gregorio845a5452010-09-08 13:50:34 -0400415 """
416
417 if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
418 verifier = verifier['oauth_verifier']
419
420 token = oauth.Token(
421 self.request_token['oauth_token'],
Joe Gregorio67d77772010-09-01 16:45:45 -0400422 self.request_token['oauth_token_secret'])
Joe Gregorio845a5452010-09-08 13:50:34 -0400423 token.set_verifier(verifier)
Joe Gregorio67d77772010-09-01 16:45:45 -0400424 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
425 client = oauth.Client(consumer, token)
426
427 headers = {
428 'user-agent': self.user_agent,
429 'content-type': 'application/x-www-form-urlencoded'
430 }
431
432 uri = _oauth_uri('access', self.discovery, self.params)
433 resp, content = client.request(uri, 'POST', headers=headers)
Joe Gregorio845a5452010-09-08 13:50:34 -0400434 if resp['status'] != '200':
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500435 logging.error('Failed to retrieve access token: %s', content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700436 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400437
Joe Gregorio845a5452010-09-08 13:50:34 -0400438 oauth_params = dict(parse_qsl(content))
439 token = oauth.Token(
440 oauth_params['oauth_token'],
441 oauth_params['oauth_token_secret'])
442
443 return OAuthCredentials(consumer, token, self.user_agent)