| OLD | NEW |
| (Empty) |
| 1 # 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. | |
| 14 | |
| 15 """Utilities for OAuth. | |
| 16 | |
| 17 Utilities for making it easier to work with OAuth. | |
| 18 """ | |
| 19 | |
| 20 __author__ = 'jcgregorio@google.com (Joe Gregorio)' | |
| 21 | |
| 22 | |
| 23 import copy | |
| 24 import httplib2 | |
| 25 import logging | |
| 26 import oauth2 as oauth | |
| 27 import urllib | |
| 28 import urlparse | |
| 29 | |
| 30 from oauth2client.anyjson import simplejson | |
| 31 from oauth2client.client import Credentials | |
| 32 from oauth2client.client import Flow | |
| 33 from oauth2client.client import Storage | |
| 34 | |
| 35 try: | |
| 36 from urlparse import parse_qsl | |
| 37 except ImportError: | |
| 38 from cgi import parse_qsl | |
| 39 | |
| 40 | |
| 41 class Error(Exception): | |
| 42 """Base error for this module.""" | |
| 43 pass | |
| 44 | |
| 45 | |
| 46 class RequestError(Error): | |
| 47 """Error occurred during request.""" | |
| 48 pass | |
| 49 | |
| 50 | |
| 51 class MissingParameter(Error): | |
| 52 pass | |
| 53 | |
| 54 | |
| 55 class CredentialsInvalidError(Error): | |
| 56 pass | |
| 57 | |
| 58 | |
| 59 def _abstract(): | |
| 60 raise NotImplementedError('You need to override this function') | |
| 61 | |
| 62 | |
| 63 def _oauth_uri(name, discovery, params): | |
| 64 """Look up the OAuth URI from the discovery | |
| 65 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 """ | |
| 75 if name not in ['request', 'access', 'authorize']: | |
| 76 raise KeyError(name) | |
| 77 keys = discovery[name]['parameters'].keys() | |
| 78 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 | |
| 84 | |
| 85 | |
| 86 class 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 | |
| 100 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): | |
| 122 """Trim the state down to something that can be pickled.""" | |
| 123 d = copy.copy(self.__dict__) | |
| 124 del d['store'] | |
| 125 return d | |
| 126 | |
| 127 def __setstate__(self, state): | |
| 128 """Reconstitute the state of the object from being pickled.""" | |
| 129 self.__dict__.update(state) | |
| 130 self.store = None | |
| 131 | |
| 132 def authorize(self, http): | |
| 133 """Authorize an httplib2.Http instance with these Credentials | |
| 134 | |
| 135 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'. | |
| 159 def new_request(uri, method='GET', body=None, headers=None, | |
| 160 redirections=httplib2.DEFAULT_MAX_REDIRECTS, | |
| 161 connection_type=None): | |
| 162 """Modify the request headers to add the appropriate | |
| 163 Authorization header.""" | |
| 164 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) | |
| 170 if headers is None: | |
| 171 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 | |
| 177 | |
| 178 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'] | |
| 183 | |
| 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 | |
| 192 return resp, content | |
| 193 | |
| 194 http.request = new_request | |
| 195 return http | |
| 196 | |
| 197 | |
| 198 class 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 | |
| 340 class FlowThreeLegged(Flow): | |
| 341 """Does the Three Legged Dance for OAuth 1.0a. | |
| 342 """ | |
| 343 | |
| 344 def __init__(self, discovery, consumer_key, consumer_secret, user_agent, | |
| 345 **kwargs): | |
| 346 """ | |
| 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 """ | |
| 355 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 = {} | |
| 361 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(): | |
| 367 if key not in self.params: | |
| 368 raise MissingParameter('Required parameter %s not supplied' % key) | |
| 369 | |
| 370 def step1_get_authorize_url(self, oauth_callback='oob'): | |
| 371 """Returns a URI to redirect to the provider. | |
| 372 | |
| 373 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. | |
| 381 """ | |
| 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) | |
| 391 | |
| 392 resp, content = client.request(uri, 'POST', headers=headers, | |
| 393 body=body) | |
| 394 if resp['status'] != '200': | |
| 395 logging.error('Failed to retrieve temporary authorization: %s', content) | |
| 396 raise RequestError('Invalid response %s.' % resp['status']) | |
| 397 | |
| 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 | |
| 403 return _oauth_uri('authorize', self.discovery, auth_params) | |
| 404 | |
| 405 def step2_exchange(self, verifier): | |
| 406 """Exhanges an authorized request token | |
| 407 for OAuthCredentials. | |
| 408 | |
| 409 Args: | |
| 410 verifier: string, dict - either the verifier token, or a dictionary | |
| 411 of the query parameters to the callback, which contains | |
| 412 the oauth_verifier. | |
| 413 Returns: | |
| 414 The Credentials object. | |
| 415 """ | |
| 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'], | |
| 422 self.request_token['oauth_token_secret']) | |
| 423 token.set_verifier(verifier) | |
| 424 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) | |
| 434 if resp['status'] != '200': | |
| 435 logging.error('Failed to retrieve access token: %s', content) | |
| 436 raise RequestError('Invalid response %s.' % resp['status']) | |
| 437 | |
| 438 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) | |
| OLD | NEW |