blob: bd4125b34406bdcd3166fe49901b0fd635a5cbb9 [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 Gregorio67d77772010-09-01 16:45:45 -040031
32try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -050033 from urlparse import parse_qsl
Joe Gregorio67d77772010-09-01 16:45:45 -040034except ImportError:
Joe Gregorio7c22ab22011-02-16 15:32:39 -050035 from cgi import parse_qsl
Joe Gregorio845a5452010-09-08 13:50:34 -040036
37
Tom Miller05cd4f52010-10-06 11:09:12 -070038class Error(Exception):
39 """Base error for this module."""
40 pass
41
42
43class RequestError(Error):
44 """Error occurred during request."""
45 pass
46
47
48class MissingParameter(Error):
Joe Gregorio67d77772010-09-01 16:45:45 -040049 pass
50
Joe Gregorio845a5452010-09-08 13:50:34 -040051
Joe Gregorioa0a52e42011-02-17 17:13:26 -050052class CredentialsInvalidError(Error):
53 pass
54
55
Joe Gregorio845a5452010-09-08 13:50:34 -040056def _abstract():
Joe Gregorio7943c5d2010-09-08 16:11:43 -040057 raise NotImplementedError('You need to override this function')
Joe Gregorio67d77772010-09-01 16:45:45 -040058
59
Joe Gregorio67d77772010-09-01 16:45:45 -040060def _oauth_uri(name, discovery, params):
ade@google.coma9907a22010-12-11 02:44:37 +000061 """Look up the OAuth URI from the discovery
Joe Gregorio845a5452010-09-08 13:50:34 -040062 document and add query parameters based on
63 params.
64
65 name - The name of the OAuth URI to lookup, one
66 of 'request', 'access', or 'authorize'.
67 discovery - Portion of discovery document the describes
68 the OAuth endpoints.
69 params - Dictionary that is used to form the query parameters
70 for the specified URI.
71 """
Joe Gregorio67d77772010-09-01 16:45:45 -040072 if name not in ['request', 'access', 'authorize']:
73 raise KeyError(name)
Joe Gregorio7943c5d2010-09-08 16:11:43 -040074 keys = discovery[name]['parameters'].keys()
Joe Gregorio67d77772010-09-01 16:45:45 -040075 query = {}
76 for key in keys:
77 if key in params:
78 query[key] = params[key]
79 return discovery[name]['url'] + '?' + urllib.urlencode(query)
80
Joe Gregorio845a5452010-09-08 13:50:34 -040081
82class Credentials(object):
83 """Base class for all Credentials objects.
84
85 Subclasses must define an authorize() method
86 that applies the credentials to an HTTP transport.
87 """
88
89 def authorize(self, http):
90 """Take an httplib2.Http instance (or equivalent) and
91 authorizes it for the set of credentials, usually by
92 replacing http.request() with a method that adds in
93 the appropriate headers and then delegates to the original
94 Http.request() method.
95 """
96 _abstract()
97
Joe Gregorio7a6df3a2011-01-31 21:55:21 -050098
Joe Gregorio695fdc12011-01-16 16:46:55 -050099class Flow(object):
100 """Base class for all Flow objects."""
101 pass
Joe Gregorio845a5452010-09-08 13:50:34 -0400102
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500103
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500104class Storage(object):
105 """Base class for all Storage objects.
106
107 Store and retrieve a single credential.
108 """
109
110 def get(self):
111 """Retrieve credential.
112
113 Returns:
114 apiclient.oauth.Credentials
115 """
116 _abstract()
117
118 def put(self, credentials):
119 """Write a credential.
120
121 Args:
122 credentials: Credentials, the credentials to store.
123 """
124 _abstract()
125
126
Joe Gregorio845a5452010-09-08 13:50:34 -0400127class OAuthCredentials(Credentials):
128 """Credentials object for OAuth 1.0a
129 """
130
131 def __init__(self, consumer, token, user_agent):
132 """
133 consumer - An instance of oauth.Consumer.
134 token - An instance of oauth.Token constructed with
135 the access token and secret.
136 user_agent - The HTTP User-Agent to provide for this application.
137 """
138 self.consumer = consumer
139 self.token = token
140 self.user_agent = user_agent
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500141 self.store = None
142
143 # True if the credentials have been revoked
144 self._invalid = False
145
146 @property
147 def invalid(self):
148 """True if the credentials are invalid, such as being revoked."""
149 return getattr(self, "_invalid", False)
150
151 def set_store(self, store):
152 """Set the storage for the credential.
153
154 Args:
155 store: callable, a callable that when passed a Credential
156 will store the credential back to where it came from.
157 This is needed to store the latest access_token if it
158 has been revoked.
159 """
160 self.store = store
161
162 def __getstate__(self):
Joe Gregorio825d78d2011-02-18 15:07:17 -0500163 """Trim the state down to something that can be pickled."""
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500164 d = copy.copy(self.__dict__)
165 del d['store']
166 return d
167
168 def __setstate__(self, state):
Joe Gregorio825d78d2011-02-18 15:07:17 -0500169 """Reconstitute the state of the object from being pickled."""
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500170 self.__dict__.update(state)
171 self.store = None
Joe Gregorio845a5452010-09-08 13:50:34 -0400172
173 def authorize(self, http):
Joe Gregorio0bc70912011-05-24 15:30:49 -0400174 """Authorize an httplib2.Http instance with these Credentials
175
Joe Gregorio845a5452010-09-08 13:50:34 -0400176 Args:
177 http - An instance of httplib2.Http
178 or something that acts like it.
179
180 Returns:
181 A modified instance of http that was passed in.
182
183 Example:
184
185 h = httplib2.Http()
186 h = credentials.authorize(h)
187
188 You can't create a new OAuth
189 subclass of httplib2.Authenication because
190 it never gets passed the absolute URI, which is
191 needed for signing. So instead we have to overload
192 'request' with a closure that adds in the
193 Authorization header and then calls the original version
194 of 'request()'.
195 """
196 request_orig = http.request
197 signer = oauth.SignatureMethod_HMAC_SHA1()
198
199 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400200 def new_request(uri, method='GET', body=None, headers=None,
Joe Gregorio845a5452010-09-08 13:50:34 -0400201 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
202 connection_type=None):
203 """Modify the request headers to add the appropriate
204 Authorization header."""
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500205 response_code = 302
206 http.follow_redirects = False
207 while response_code in [301, 302]:
208 req = oauth.Request.from_consumer_and_token(
209 self.consumer, self.token, http_method=method, http_url=uri)
210 req.sign_request(signer, self.consumer, self.token)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500211 if headers is None:
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500212 headers = {}
213 headers.update(req.to_header())
214 if 'user-agent' in headers:
215 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
216 else:
217 headers['user-agent'] = self.user_agent
Joe Gregorio0bc70912011-05-24 15:30:49 -0400218
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500219 resp, content = request_orig(uri, method, body, headers,
220 redirections, connection_type)
221 response_code = resp.status
222 if response_code in [301, 302]:
223 uri = resp['location']
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500224
225 # Update the stored credential if it becomes invalid.
226 if response_code == 401:
227 logging.info('Access token no longer valid: %s' % content)
228 self._invalid = True
229 if self.store is not None:
230 self.store(self)
231 raise CredentialsInvalidError("Credentials are no longer valid.")
232
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500233 return resp, content
Joe Gregorio845a5452010-09-08 13:50:34 -0400234
235 http.request = new_request
236 return http
237
238
Joe Gregorio0bc70912011-05-24 15:30:49 -0400239class TwoLeggedOAuthCredentials(Credentials):
240 """Two Legged Credentials object for OAuth 1.0a.
241
242 The Two Legged object is created directly, not from a flow. Once you
243 authorize and httplib2.Http instance you can change the requestor and that
244 change will propogate to the authorized httplib2.Http instance. For example:
245
246 http = httplib2.Http()
247 http = credentials.authorize(http)
248
249 credentials.requestor = 'foo@example.info'
250 http.request(...)
251 credentials.requestor = 'bar@example.info'
252 http.request(...)
253 """
254
255 def __init__(self, consumer_key, consumer_secret, user_agent):
256 """
257 Args:
258 consumer_key: string, An OAuth 1.0 consumer key
259 consumer_secret: string, An OAuth 1.0 consumer secret
260 user_agent: string, The HTTP User-Agent to provide for this application.
261 """
262 self.consumer = oauth.Consumer(consumer_key, consumer_secret)
263 self.user_agent = user_agent
264 self.store = None
265
266 # email address of the user to act on the behalf of.
267 self._requestor = None
268
269 @property
270 def invalid(self):
271 """True if the credentials are invalid, such as being revoked.
272
273 Always returns False for Two Legged Credentials.
274 """
275 return False
276
277 def getrequestor(self):
278 return self._requestor
279
280 def setrequestor(self, email):
281 self._requestor = email
282
283 requestor = property(getrequestor, setrequestor, None,
284 'The email address of the user to act on behalf of')
285
286 def set_store(self, store):
287 """Set the storage for the credential.
288
289 Args:
290 store: callable, a callable that when passed a Credential
291 will store the credential back to where it came from.
292 This is needed to store the latest access_token if it
293 has been revoked.
294 """
295 self.store = store
296
297 def __getstate__(self):
298 """Trim the state down to something that can be pickled."""
299 d = copy.copy(self.__dict__)
300 del d['store']
301 return d
302
303 def __setstate__(self, state):
304 """Reconstitute the state of the object from being pickled."""
305 self.__dict__.update(state)
306 self.store = None
307
308 def authorize(self, http):
309 """Authorize an httplib2.Http instance with these Credentials
310
311 Args:
312 http - An instance of httplib2.Http
313 or something that acts like it.
314
315 Returns:
316 A modified instance of http that was passed in.
317
318 Example:
319
320 h = httplib2.Http()
321 h = credentials.authorize(h)
322
323 You can't create a new OAuth
324 subclass of httplib2.Authenication because
325 it never gets passed the absolute URI, which is
326 needed for signing. So instead we have to overload
327 'request' with a closure that adds in the
328 Authorization header and then calls the original version
329 of 'request()'.
330 """
331 request_orig = http.request
332 signer = oauth.SignatureMethod_HMAC_SHA1()
333
334 # The closure that will replace 'httplib2.Http.request'.
335 def new_request(uri, method='GET', body=None, headers=None,
336 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
337 connection_type=None):
338 """Modify the request headers to add the appropriate
339 Authorization header."""
340 response_code = 302
341 http.follow_redirects = False
342 while response_code in [301, 302]:
343 # add in xoauth_requestor_id=self._requestor to the uri
344 if self._requestor is None:
345 raise MissingParameter(
346 'Requestor must be set before using TwoLeggedOAuthCredentials')
347 parsed = list(urlparse.urlparse(uri))
348 q = parse_qsl(parsed[4])
349 q.append(('xoauth_requestor_id', self._requestor))
350 parsed[4] = urllib.urlencode(q)
351 uri = urlparse.urlunparse(parsed)
352
353 req = oauth.Request.from_consumer_and_token(
354 self.consumer, None, http_method=method, http_url=uri)
355 req.sign_request(signer, self.consumer, None)
356 if headers is None:
357 headers = {}
358 headers.update(req.to_header())
359 if 'user-agent' in headers:
360 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
361 else:
362 headers['user-agent'] = self.user_agent
363 resp, content = request_orig(uri, method, body, headers,
364 redirections, connection_type)
365 response_code = resp.status
366 if response_code in [301, 302]:
367 uri = resp['location']
368
369 if response_code == 401:
370 logging.info('Access token no longer valid: %s' % content)
371 # Do not store the invalid state of the Credentials because
372 # being 2LO they could be reinstated in the future.
373 raise CredentialsInvalidError("Credentials are invalid.")
374
375 return resp, content
376
377 http.request = new_request
378 return http
379
380
Joe Gregorio695fdc12011-01-16 16:46:55 -0500381class FlowThreeLegged(Flow):
Joe Gregorio845a5452010-09-08 13:50:34 -0400382 """Does the Three Legged Dance for OAuth 1.0a.
383 """
384
Joe Gregorio67d77772010-09-01 16:45:45 -0400385 def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
386 **kwargs):
Joe Gregorio845a5452010-09-08 13:50:34 -0400387 """
388 discovery - Section of the API discovery document that describes
389 the OAuth endpoints.
390 consumer_key - OAuth consumer key
391 consumer_secret - OAuth consumer secret
392 user_agent - The HTTP User-Agent that identifies the application.
393 **kwargs - The keyword arguments are all optional and required
394 parameters for the OAuth calls.
395 """
Joe Gregorio67d77772010-09-01 16:45:45 -0400396 self.discovery = discovery
397 self.consumer_key = consumer_key
398 self.consumer_secret = consumer_secret
399 self.user_agent = user_agent
400 self.params = kwargs
401 self.request_token = {}
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400402 required = {}
403 for uriinfo in discovery.itervalues():
404 for name, value in uriinfo['parameters'].iteritems():
405 if value['required'] and not name.startswith('oauth_'):
406 required[name] = 1
407 for key in required.iterkeys():
Joe Gregorio67d77772010-09-01 16:45:45 -0400408 if key not in self.params:
409 raise MissingParameter('Required parameter %s not supplied' % key)
410
Joe Gregorio845a5452010-09-08 13:50:34 -0400411 def step1_get_authorize_url(self, oauth_callback='oob'):
Joe Gregorio67d77772010-09-01 16:45:45 -0400412 """Returns a URI to redirect to the provider.
413
Joe Gregorio845a5452010-09-08 13:50:34 -0400414 oauth_callback - Either the string 'oob' for a non-web-based application,
415 or a URI that handles the callback from the authorization
416 server.
417
418 If oauth_callback is 'oob' then pass in the
419 generated verification code to step2_exchange,
420 otherwise pass in the query parameters received
421 at the callback uri to step2_exchange.
Joe Gregorio67d77772010-09-01 16:45:45 -0400422 """
423 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
424 client = oauth.Client(consumer)
425
426 headers = {
427 'user-agent': self.user_agent,
428 'content-type': 'application/x-www-form-urlencoded'
429 }
430 body = urllib.urlencode({'oauth_callback': oauth_callback})
431 uri = _oauth_uri('request', self.discovery, self.params)
Joe Gregorio845a5452010-09-08 13:50:34 -0400432
Joe Gregorio67d77772010-09-01 16:45:45 -0400433 resp, content = client.request(uri, 'POST', headers=headers,
434 body=body)
435 if resp['status'] != '200':
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500436 logging.error('Failed to retrieve temporary authorization: %s', content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700437 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400438
439 self.request_token = dict(parse_qsl(content))
440
441 auth_params = copy.copy(self.params)
442 auth_params['oauth_token'] = self.request_token['oauth_token']
443
Joe Gregorio845a5452010-09-08 13:50:34 -0400444 return _oauth_uri('authorize', self.discovery, auth_params)
Joe Gregorio67d77772010-09-01 16:45:45 -0400445
Joe Gregorio845a5452010-09-08 13:50:34 -0400446 def step2_exchange(self, verifier):
447 """Exhanges an authorized request token
448 for OAuthCredentials.
Joe Gregorio67d77772010-09-01 16:45:45 -0400449
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500450 Args:
451 verifier: string, dict - either the verifier token, or a dictionary
Joe Gregorio845a5452010-09-08 13:50:34 -0400452 of the query parameters to the callback, which contains
453 the oauth_verifier.
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500454 Returns:
455 The Credentials object.
Joe Gregorio845a5452010-09-08 13:50:34 -0400456 """
457
458 if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
459 verifier = verifier['oauth_verifier']
460
461 token = oauth.Token(
462 self.request_token['oauth_token'],
Joe Gregorio67d77772010-09-01 16:45:45 -0400463 self.request_token['oauth_token_secret'])
Joe Gregorio845a5452010-09-08 13:50:34 -0400464 token.set_verifier(verifier)
Joe Gregorio67d77772010-09-01 16:45:45 -0400465 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
466 client = oauth.Client(consumer, token)
467
468 headers = {
469 'user-agent': self.user_agent,
470 'content-type': 'application/x-www-form-urlencoded'
471 }
472
473 uri = _oauth_uri('access', self.discovery, self.params)
474 resp, content = client.request(uri, 'POST', headers=headers)
Joe Gregorio845a5452010-09-08 13:50:34 -0400475 if resp['status'] != '200':
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500476 logging.error('Failed to retrieve access token: %s', content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700477 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400478
Joe Gregorio845a5452010-09-08 13:50:34 -0400479 oauth_params = dict(parse_qsl(content))
480 token = oauth.Token(
481 oauth_params['oauth_token'],
482 oauth_params['oauth_token_secret'])
483
484 return OAuthCredentials(consumer, token, self.user_agent)