blob: 11eb6804a9cac7f5f05494a970621517a701a356 [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 Gregorio695fdc12011-01-16 16:46:55 -050029from anyjson import simplejson
Joe Gregorio67d77772010-09-01 16:45:45 -040030
31try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -050032 from urlparse import parse_qsl
Joe Gregorio67d77772010-09-01 16:45:45 -040033except ImportError:
Joe Gregorio7c22ab22011-02-16 15:32:39 -050034 from cgi import parse_qsl
Joe Gregorio845a5452010-09-08 13:50:34 -040035
36
Tom Miller05cd4f52010-10-06 11:09:12 -070037class Error(Exception):
38 """Base error for this module."""
39 pass
40
41
42class RequestError(Error):
43 """Error occurred during request."""
44 pass
45
46
47class MissingParameter(Error):
Joe Gregorio67d77772010-09-01 16:45:45 -040048 pass
49
Joe Gregorio845a5452010-09-08 13:50:34 -040050
Joe Gregorioa0a52e42011-02-17 17:13:26 -050051class CredentialsInvalidError(Error):
52 pass
53
54
Joe Gregorio845a5452010-09-08 13:50:34 -040055def _abstract():
Joe Gregorio7943c5d2010-09-08 16:11:43 -040056 raise NotImplementedError('You need to override this function')
Joe Gregorio67d77772010-09-01 16:45:45 -040057
58
Joe Gregorio67d77772010-09-01 16:45:45 -040059def _oauth_uri(name, discovery, params):
ade@google.coma9907a22010-12-11 02:44:37 +000060 """Look up the OAuth URI from the discovery
Joe Gregorio845a5452010-09-08 13:50:34 -040061 document and add query parameters based on
62 params.
63
64 name - The name of the OAuth URI to lookup, one
65 of 'request', 'access', or 'authorize'.
66 discovery - Portion of discovery document the describes
67 the OAuth endpoints.
68 params - Dictionary that is used to form the query parameters
69 for the specified URI.
70 """
Joe Gregorio67d77772010-09-01 16:45:45 -040071 if name not in ['request', 'access', 'authorize']:
72 raise KeyError(name)
Joe Gregorio7943c5d2010-09-08 16:11:43 -040073 keys = discovery[name]['parameters'].keys()
Joe Gregorio67d77772010-09-01 16:45:45 -040074 query = {}
75 for key in keys:
76 if key in params:
77 query[key] = params[key]
78 return discovery[name]['url'] + '?' + urllib.urlencode(query)
79
Joe Gregorio845a5452010-09-08 13:50:34 -040080
81class Credentials(object):
82 """Base class for all Credentials objects.
83
84 Subclasses must define an authorize() method
85 that applies the credentials to an HTTP transport.
86 """
87
88 def authorize(self, http):
89 """Take an httplib2.Http instance (or equivalent) and
90 authorizes it for the set of credentials, usually by
91 replacing http.request() with a method that adds in
92 the appropriate headers and then delegates to the original
93 Http.request() method.
94 """
95 _abstract()
96
Joe Gregorio7a6df3a2011-01-31 21:55:21 -050097
Joe Gregorio695fdc12011-01-16 16:46:55 -050098class Flow(object):
99 """Base class for all Flow objects."""
100 pass
Joe Gregorio845a5452010-09-08 13:50:34 -0400101
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500102
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500103class Storage(object):
104 """Base class for all Storage objects.
105
106 Store and retrieve a single credential.
107 """
108
109 def get(self):
110 """Retrieve credential.
111
112 Returns:
113 apiclient.oauth.Credentials
114 """
115 _abstract()
116
117 def put(self, credentials):
118 """Write a credential.
119
120 Args:
121 credentials: Credentials, the credentials to store.
122 """
123 _abstract()
124
125
Joe Gregorio845a5452010-09-08 13:50:34 -0400126class OAuthCredentials(Credentials):
127 """Credentials object for OAuth 1.0a
128 """
129
130 def __init__(self, consumer, token, user_agent):
131 """
132 consumer - An instance of oauth.Consumer.
133 token - An instance of oauth.Token constructed with
134 the access token and secret.
135 user_agent - The HTTP User-Agent to provide for this application.
136 """
137 self.consumer = consumer
138 self.token = token
139 self.user_agent = user_agent
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500140 self.store = None
141
142 # True if the credentials have been revoked
143 self._invalid = False
144
145 @property
146 def invalid(self):
147 """True if the credentials are invalid, such as being revoked."""
148 return getattr(self, "_invalid", False)
149
150 def set_store(self, store):
151 """Set the storage for the credential.
152
153 Args:
154 store: callable, a callable that when passed a Credential
155 will store the credential back to where it came from.
156 This is needed to store the latest access_token if it
157 has been revoked.
158 """
159 self.store = store
160
161 def __getstate__(self):
Joe Gregorio825d78d2011-02-18 15:07:17 -0500162 """Trim the state down to something that can be pickled."""
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500163 d = copy.copy(self.__dict__)
164 del d['store']
165 return d
166
167 def __setstate__(self, state):
Joe Gregorio825d78d2011-02-18 15:07:17 -0500168 """Reconstitute the state of the object from being pickled."""
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500169 self.__dict__.update(state)
170 self.store = None
Joe Gregorio845a5452010-09-08 13:50:34 -0400171
172 def authorize(self, http):
Joe Gregorio0bc70912011-05-24 15:30:49 -0400173 """Authorize an httplib2.Http instance with these Credentials
174
Joe Gregorio845a5452010-09-08 13:50:34 -0400175 Args:
176 http - An instance of httplib2.Http
177 or something that acts like it.
178
179 Returns:
180 A modified instance of http that was passed in.
181
182 Example:
183
184 h = httplib2.Http()
185 h = credentials.authorize(h)
186
187 You can't create a new OAuth
188 subclass of httplib2.Authenication because
189 it never gets passed the absolute URI, which is
190 needed for signing. So instead we have to overload
191 'request' with a closure that adds in the
192 Authorization header and then calls the original version
193 of 'request()'.
194 """
195 request_orig = http.request
196 signer = oauth.SignatureMethod_HMAC_SHA1()
197
198 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400199 def new_request(uri, method='GET', body=None, headers=None,
Joe Gregorio845a5452010-09-08 13:50:34 -0400200 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
201 connection_type=None):
202 """Modify the request headers to add the appropriate
203 Authorization header."""
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500204 response_code = 302
205 http.follow_redirects = False
206 while response_code in [301, 302]:
207 req = oauth.Request.from_consumer_and_token(
208 self.consumer, self.token, http_method=method, http_url=uri)
209 req.sign_request(signer, self.consumer, self.token)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500210 if headers is None:
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500211 headers = {}
212 headers.update(req.to_header())
213 if 'user-agent' in headers:
214 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
215 else:
216 headers['user-agent'] = self.user_agent
Joe Gregorio0bc70912011-05-24 15:30:49 -0400217
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500218 resp, content = request_orig(uri, method, body, headers,
219 redirections, connection_type)
220 response_code = resp.status
221 if response_code in [301, 302]:
222 uri = resp['location']
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500223
224 # Update the stored credential if it becomes invalid.
225 if response_code == 401:
226 logging.info('Access token no longer valid: %s' % content)
227 self._invalid = True
228 if self.store is not None:
229 self.store(self)
230 raise CredentialsInvalidError("Credentials are no longer valid.")
231
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500232 return resp, content
Joe Gregorio845a5452010-09-08 13:50:34 -0400233
234 http.request = new_request
235 return http
236
237
Joe Gregorio0bc70912011-05-24 15:30:49 -0400238class TwoLeggedOAuthCredentials(Credentials):
239 """Two Legged Credentials object for OAuth 1.0a.
240
241 The Two Legged object is created directly, not from a flow. Once you
242 authorize and httplib2.Http instance you can change the requestor and that
243 change will propogate to the authorized httplib2.Http instance. For example:
244
245 http = httplib2.Http()
246 http = credentials.authorize(http)
247
248 credentials.requestor = 'foo@example.info'
249 http.request(...)
250 credentials.requestor = 'bar@example.info'
251 http.request(...)
252 """
253
254 def __init__(self, consumer_key, consumer_secret, user_agent):
255 """
256 Args:
257 consumer_key: string, An OAuth 1.0 consumer key
258 consumer_secret: string, An OAuth 1.0 consumer secret
259 user_agent: string, The HTTP User-Agent to provide for this application.
260 """
261 self.consumer = oauth.Consumer(consumer_key, consumer_secret)
262 self.user_agent = user_agent
263 self.store = None
264
265 # email address of the user to act on the behalf of.
266 self._requestor = None
267
268 @property
269 def invalid(self):
270 """True if the credentials are invalid, such as being revoked.
271
272 Always returns False for Two Legged Credentials.
273 """
274 return False
275
276 def getrequestor(self):
277 return self._requestor
278
279 def setrequestor(self, email):
280 self._requestor = email
281
282 requestor = property(getrequestor, setrequestor, None,
283 'The email address of the user to act on behalf of')
284
285 def set_store(self, store):
286 """Set the storage for the credential.
287
288 Args:
289 store: callable, a callable that when passed a Credential
290 will store the credential back to where it came from.
291 This is needed to store the latest access_token if it
292 has been revoked.
293 """
294 self.store = store
295
296 def __getstate__(self):
297 """Trim the state down to something that can be pickled."""
298 d = copy.copy(self.__dict__)
299 del d['store']
300 return d
301
302 def __setstate__(self, state):
303 """Reconstitute the state of the object from being pickled."""
304 self.__dict__.update(state)
305 self.store = None
306
307 def authorize(self, http):
308 """Authorize an httplib2.Http instance with these Credentials
309
310 Args:
311 http - An instance of httplib2.Http
312 or something that acts like it.
313
314 Returns:
315 A modified instance of http that was passed in.
316
317 Example:
318
319 h = httplib2.Http()
320 h = credentials.authorize(h)
321
322 You can't create a new OAuth
323 subclass of httplib2.Authenication because
324 it never gets passed the absolute URI, which is
325 needed for signing. So instead we have to overload
326 'request' with a closure that adds in the
327 Authorization header and then calls the original version
328 of 'request()'.
329 """
330 request_orig = http.request
331 signer = oauth.SignatureMethod_HMAC_SHA1()
332
333 # The closure that will replace 'httplib2.Http.request'.
334 def new_request(uri, method='GET', body=None, headers=None,
335 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
336 connection_type=None):
337 """Modify the request headers to add the appropriate
338 Authorization header."""
339 response_code = 302
340 http.follow_redirects = False
341 while response_code in [301, 302]:
342 # add in xoauth_requestor_id=self._requestor to the uri
343 if self._requestor is None:
344 raise MissingParameter(
345 'Requestor must be set before using TwoLeggedOAuthCredentials')
346 parsed = list(urlparse.urlparse(uri))
347 q = parse_qsl(parsed[4])
348 q.append(('xoauth_requestor_id', self._requestor))
349 parsed[4] = urllib.urlencode(q)
350 uri = urlparse.urlunparse(parsed)
351
352 req = oauth.Request.from_consumer_and_token(
353 self.consumer, None, http_method=method, http_url=uri)
354 req.sign_request(signer, self.consumer, None)
355 if headers is None:
356 headers = {}
357 headers.update(req.to_header())
358 if 'user-agent' in headers:
359 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
360 else:
361 headers['user-agent'] = self.user_agent
362 resp, content = request_orig(uri, method, body, headers,
363 redirections, connection_type)
364 response_code = resp.status
365 if response_code in [301, 302]:
366 uri = resp['location']
367
368 if response_code == 401:
369 logging.info('Access token no longer valid: %s' % content)
370 # Do not store the invalid state of the Credentials because
371 # being 2LO they could be reinstated in the future.
372 raise CredentialsInvalidError("Credentials are invalid.")
373
374 return resp, content
375
376 http.request = new_request
377 return http
378
379
Joe Gregorio695fdc12011-01-16 16:46:55 -0500380class FlowThreeLegged(Flow):
Joe Gregorio845a5452010-09-08 13:50:34 -0400381 """Does the Three Legged Dance for OAuth 1.0a.
382 """
383
Joe Gregorio67d77772010-09-01 16:45:45 -0400384 def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
385 **kwargs):
Joe Gregorio845a5452010-09-08 13:50:34 -0400386 """
387 discovery - Section of the API discovery document that describes
388 the OAuth endpoints.
389 consumer_key - OAuth consumer key
390 consumer_secret - OAuth consumer secret
391 user_agent - The HTTP User-Agent that identifies the application.
392 **kwargs - The keyword arguments are all optional and required
393 parameters for the OAuth calls.
394 """
Joe Gregorio67d77772010-09-01 16:45:45 -0400395 self.discovery = discovery
396 self.consumer_key = consumer_key
397 self.consumer_secret = consumer_secret
398 self.user_agent = user_agent
399 self.params = kwargs
400 self.request_token = {}
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400401 required = {}
402 for uriinfo in discovery.itervalues():
403 for name, value in uriinfo['parameters'].iteritems():
404 if value['required'] and not name.startswith('oauth_'):
405 required[name] = 1
406 for key in required.iterkeys():
Joe Gregorio67d77772010-09-01 16:45:45 -0400407 if key not in self.params:
408 raise MissingParameter('Required parameter %s not supplied' % key)
409
Joe Gregorio845a5452010-09-08 13:50:34 -0400410 def step1_get_authorize_url(self, oauth_callback='oob'):
Joe Gregorio67d77772010-09-01 16:45:45 -0400411 """Returns a URI to redirect to the provider.
412
Joe Gregorio845a5452010-09-08 13:50:34 -0400413 oauth_callback - Either the string 'oob' for a non-web-based application,
414 or a URI that handles the callback from the authorization
415 server.
416
417 If oauth_callback is 'oob' then pass in the
418 generated verification code to step2_exchange,
419 otherwise pass in the query parameters received
420 at the callback uri to step2_exchange.
Joe Gregorio67d77772010-09-01 16:45:45 -0400421 """
422 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
423 client = oauth.Client(consumer)
424
425 headers = {
426 'user-agent': self.user_agent,
427 'content-type': 'application/x-www-form-urlencoded'
428 }
429 body = urllib.urlencode({'oauth_callback': oauth_callback})
430 uri = _oauth_uri('request', self.discovery, self.params)
Joe Gregorio845a5452010-09-08 13:50:34 -0400431
Joe Gregorio67d77772010-09-01 16:45:45 -0400432 resp, content = client.request(uri, 'POST', headers=headers,
433 body=body)
434 if resp['status'] != '200':
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500435 logging.error('Failed to retrieve temporary authorization: %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
438 self.request_token = dict(parse_qsl(content))
439
440 auth_params = copy.copy(self.params)
441 auth_params['oauth_token'] = self.request_token['oauth_token']
442
Joe Gregorio845a5452010-09-08 13:50:34 -0400443 return _oauth_uri('authorize', self.discovery, auth_params)
Joe Gregorio67d77772010-09-01 16:45:45 -0400444
Joe Gregorio845a5452010-09-08 13:50:34 -0400445 def step2_exchange(self, verifier):
446 """Exhanges an authorized request token
447 for OAuthCredentials.
Joe Gregorio67d77772010-09-01 16:45:45 -0400448
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500449 Args:
450 verifier: string, dict - either the verifier token, or a dictionary
Joe Gregorio845a5452010-09-08 13:50:34 -0400451 of the query parameters to the callback, which contains
452 the oauth_verifier.
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500453 Returns:
454 The Credentials object.
Joe Gregorio845a5452010-09-08 13:50:34 -0400455 """
456
457 if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
458 verifier = verifier['oauth_verifier']
459
460 token = oauth.Token(
461 self.request_token['oauth_token'],
Joe Gregorio67d77772010-09-01 16:45:45 -0400462 self.request_token['oauth_token_secret'])
Joe Gregorio845a5452010-09-08 13:50:34 -0400463 token.set_verifier(verifier)
Joe Gregorio67d77772010-09-01 16:45:45 -0400464 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
465 client = oauth.Client(consumer, token)
466
467 headers = {
468 'user-agent': self.user_agent,
469 'content-type': 'application/x-www-form-urlencoded'
470 }
471
472 uri = _oauth_uri('access', self.discovery, self.params)
473 resp, content = client.request(uri, 'POST', headers=headers)
Joe Gregorio845a5452010-09-08 13:50:34 -0400474 if resp['status'] != '200':
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500475 logging.error('Failed to retrieve access token: %s', content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700476 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400477
Joe Gregorio845a5452010-09-08 13:50:34 -0400478 oauth_params = dict(parse_qsl(content))
479 token = oauth.Token(
480 oauth_params['oauth_token'],
481 oauth_params['oauth_token_secret'])
482
483 return OAuthCredentials(consumer, token, self.user_agent)