Package restkit :: Module oauth2
[hide private]
[frames] | no frames]

Source Code for Module restkit.oauth2

  1  # -*- coding: utf-8 - 
  2  # 
  3  # This file is part of restkit released under the MIT license.  
  4  # See the NOTICE for more information. 
  5   
  6  import base64 
  7  import urllib 
  8  import time 
  9  import random 
 10  import urlparse 
 11  import hmac 
 12  import binascii 
 13   
 14  try: 
 15      from urlparse import parse_qs, parse_qsl 
 16  except ImportError: 
 17      from cgi import parse_qs, parse_qsl 
 18   
 19  from restkit.util import to_bytestring 
 20   
 21   
 22  try: 
 23      from hashlib import sha1 
 24      sha = sha1 
 25  except ImportError: 
 26      # hashlib was added in Python 2.5 
 27      import sha 
 28   
 29  from restkit.version import __version__ 
 30   
 31  OAUTH_VERSION = '1.0'  # Hi Blaine! 
 32  HTTP_METHOD = 'GET' 
 33  SIGNATURE_METHOD = 'PLAINTEXT' 
34 35 36 -class Error(RuntimeError):
37 """Generic exception class.""" 38
39 - def __init__(self, message='OAuth error occurred.'):
40 self._message = message
41 42 @property
43 - def message(self):
44 """A hack to get around the deprecation errors in 2.6.""" 45 return self._message
46
47 - def __str__(self):
48 return self._message
49
50 51 -class MissingSignature(Error):
52 pass
53
54 55 -def build_authenticate_header(realm=''):
56 """Optional WWW-Authenticate header (401 error)""" 57 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
58
59 60 -def build_xoauth_string(url, consumer, token=None):
61 """Build an XOAUTH string for use in SMTP/IMPA authentication.""" 62 request = Request.from_consumer_and_token(consumer, token, 63 "GET", url) 64 65 signing_method = SignatureMethod_HMAC_SHA1() 66 request.sign_request(signing_method, consumer, token) 67 68 params = [] 69 for k, v in sorted(request.iteritems()): 70 if v is not None: 71 params.append('%s="%s"' % (k, escape(v))) 72 73 return "%s %s %s" % ("GET", url, ','.join(params))
74
75 76 -def to_unicode(s):
77 """ Convert to unicode, raise exception with instructive error 78 message if s is not unicode, ascii, or utf-8. """ 79 if not isinstance(s, unicode): 80 if not isinstance(s, str): 81 raise TypeError('You are required to pass either unicode or string here, not: %r (%s)' % (type(s), s)) 82 try: 83 s = s.decode('utf-8') 84 except UnicodeDecodeError, le: 85 raise TypeError('You are required to pass either a unicode object or a utf-8 string here. You passed a Python string object which contained non-utf-8: %r. The UnicodeDecodeError that resulted from attempting to interpret it as utf-8 was: %s' % (s, le,)) 86 return s
87
88 -def to_utf8(s):
89 return to_unicode(s).encode('utf-8')
90
91 -def to_unicode_if_string(s):
92 if isinstance(s, basestring): 93 return to_unicode(s) 94 else: 95 return s
96
97 -def to_utf8_if_string(s):
98 if isinstance(s, basestring): 99 return to_utf8(s) 100 else: 101 return s
102
103 -def to_unicode_optional_iterator(x):
104 """ 105 Raise TypeError if x is a str containing non-utf8 bytes or if x is 106 an iterable which contains such a str. 107 """ 108 if isinstance(x, basestring): 109 return to_unicode(x) 110 111 try: 112 l = list(x) 113 except TypeError, e: 114 assert 'is not iterable' in str(e) 115 return x 116 else: 117 return [ to_unicode(e) for e in l ]
118
119 -def to_utf8_optional_iterator(x):
120 """ 121 Raise TypeError if x is a str or if x is an iterable which 122 contains a str. 123 """ 124 if isinstance(x, basestring): 125 return to_utf8(x) 126 127 try: 128 l = list(x) 129 except TypeError, e: 130 assert 'is not iterable' in str(e) 131 return x 132 else: 133 return [ to_utf8_if_string(e) for e in l ]
134
135 -def escape(s):
136 """Escape a URL including any /.""" 137 return urllib.quote(s.encode('utf-8'), safe='~')
138
139 -def generate_timestamp():
140 """Get seconds since epoch (UTC).""" 141 return int(time.time())
142
143 144 -def generate_nonce(length=8):
145 """Generate pseudorandom number.""" 146 return ''.join([str(random.randint(0, 9)) for i in range(length)])
147
148 149 -def generate_verifier(length=8):
150 """Generate pseudorandom number.""" 151 return ''.join([str(random.randint(0, 9)) for i in range(length)])
152
153 154 -class Consumer(object):
155 """A consumer of OAuth-protected services. 156 157 The OAuth consumer is a "third-party" service that wants to access 158 protected resources from an OAuth service provider on behalf of an end 159 user. It's kind of the OAuth client. 160 161 Usually a consumer must be registered with the service provider by the 162 developer of the consumer software. As part of that process, the service 163 provider gives the consumer a *key* and a *secret* with which the consumer 164 software can identify itself to the service. The consumer will include its 165 key in each request to identify itself, but will use its secret only when 166 signing requests, to prove that the request is from that particular 167 registered consumer. 168 169 Once registered, the consumer can then use its consumer credentials to ask 170 the service provider for a request token, kicking off the OAuth 171 authorization process. 172 """ 173 174 key = None 175 secret = None 176
177 - def __init__(self, key, secret):
178 self.key = key 179 self.secret = secret 180 181 if self.key is None or self.secret is None: 182 raise ValueError("Key and secret must be set.")
183
184 - def __str__(self):
185 data = {'oauth_consumer_key': self.key, 186 'oauth_consumer_secret': self.secret} 187 188 return urllib.urlencode(data)
189
190 191 -class Token(object):
192 """An OAuth credential used to request authorization or a protected 193 resource. 194 195 Tokens in OAuth comprise a *key* and a *secret*. The key is included in 196 requests to identify the token being used, but the secret is used only in 197 the signature, to prove that the requester is who the server gave the 198 token to. 199 200 When first negotiating the authorization, the consumer asks for a *request 201 token* that the live user authorizes with the service provider. The 202 consumer then exchanges the request token for an *access token* that can 203 be used to access protected resources. 204 """ 205 206 key = None 207 secret = None 208 callback = None 209 callback_confirmed = None 210 verifier = None 211
212 - def __init__(self, key, secret):
213 self.key = key 214 self.secret = secret 215 216 if self.key is None or self.secret is None: 217 raise ValueError("Key and secret must be set.")
218
219 - def set_callback(self, callback):
220 self.callback = callback 221 self.callback_confirmed = 'true'
222
223 - def set_verifier(self, verifier=None):
224 if verifier is not None: 225 self.verifier = verifier 226 else: 227 self.verifier = generate_verifier()
228
229 - def get_callback_url(self):
230 if self.callback and self.verifier: 231 # Append the oauth_verifier. 232 parts = urlparse.urlparse(self.callback) 233 scheme, netloc, path, params, query, fragment = parts[:6] 234 if query: 235 query = '%s&oauth_verifier=%s' % (query, self.verifier) 236 else: 237 query = 'oauth_verifier=%s' % self.verifier 238 return urlparse.urlunparse((scheme, netloc, path, params, 239 query, fragment)) 240 return self.callback
241
242 - def to_string(self):
243 """Returns this token as a plain string, suitable for storage. 244 245 The resulting string includes the token's secret, so you should never 246 send or store this string where a third party can read it. 247 """ 248 249 data = { 250 'oauth_token': self.key, 251 'oauth_token_secret': self.secret, 252 } 253 254 if self.callback_confirmed is not None: 255 data['oauth_callback_confirmed'] = self.callback_confirmed 256 return urllib.urlencode(data)
257 258 @staticmethod
259 - def from_string(s):
260 """Deserializes a token from a string like one returned by 261 `to_string()`.""" 262 263 if not len(s): 264 raise ValueError("Invalid parameter string.") 265 266 params = parse_qs(s, keep_blank_values=False) 267 if not len(params): 268 raise ValueError("Invalid parameter string.") 269 270 try: 271 key = params['oauth_token'][0] 272 except Exception: 273 raise ValueError("'oauth_token' not found in OAuth request.") 274 275 try: 276 secret = params['oauth_token_secret'][0] 277 except Exception: 278 raise ValueError("'oauth_token_secret' not found in " 279 "OAuth request.") 280 281 token = Token(key, secret) 282 try: 283 token.callback_confirmed = params['oauth_callback_confirmed'][0] 284 except KeyError: 285 pass # 1.0, no callback confirmed. 286 return token
287
288 - def __str__(self):
289 return self.to_string()
290
291 292 -def setter(attr):
293 name = attr.__name__ 294 295 def getter(self): 296 try: 297 return self.__dict__[name] 298 except KeyError: 299 raise AttributeError(name)
300 301 def deleter(self): 302 del self.__dict__[name] 303 304 return property(getter, attr, deleter) 305
306 307 -class Request(dict):
308 309 """The parameters and information for an HTTP request, suitable for 310 authorizing with OAuth credentials. 311 312 When a consumer wants to access a service's protected resources, it does 313 so using a signed HTTP request identifying itself (the consumer) with its 314 key, and providing an access token authorized by the end user to access 315 those resources. 316 317 """ 318 319 version = OAUTH_VERSION 320
321 - def __init__(self, method=HTTP_METHOD, url=None, parameters=None, 322 body='', is_form_encoded=False):
323 if url is not None: 324 self.url = to_unicode(url) 325 self.method = method 326 if parameters is not None: 327 for k, v in parameters.iteritems(): 328 k = to_unicode(k) 329 v = to_unicode_optional_iterator(v) 330 self[k] = v 331 self.body = body 332 self.is_form_encoded = is_form_encoded
333 334 335 @setter
336 - def url(self, value):
337 self.__dict__['url'] = value 338 if value is not None: 339 scheme, netloc, path, params, query, fragment = urlparse.urlparse(value) 340 341 # Exclude default port numbers. 342 if scheme == 'http' and netloc[-3:] == ':80': 343 netloc = netloc[:-3] 344 elif scheme == 'https' and netloc[-4:] == ':443': 345 netloc = netloc[:-4] 346 if scheme not in ('http', 'https'): 347 raise ValueError("Unsupported URL %s (%s)." % (value, scheme)) 348 349 # Normalized URL excludes params, query, and fragment. 350 self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None)) 351 else: 352 self.normalized_url = None 353 self.__dict__['url'] = None
354 355 @setter
356 - def method(self, value):
357 self.__dict__['method'] = value.upper()
358
359 - def _get_timestamp_nonce(self):
360 return self['oauth_timestamp'], self['oauth_nonce']
361
362 - def get_nonoauth_parameters(self):
363 """Get any non-OAuth parameters.""" 364 return dict([(k, v) for k, v in self.iteritems() 365 if not k.startswith('oauth_')])
366
367 - def to_header(self, realm=''):
368 """Serialize as a header for an HTTPAuth request.""" 369 oauth_params = ((k, v) for k, v in self.items() 370 if k.startswith('oauth_')) 371 stringy_params = ((k, escape(str(v))) for k, v in oauth_params) 372 header_params = ('%s="%s"' % (k, v) for k, v in stringy_params) 373 params_header = ', '.join(header_params) 374 375 auth_header = 'OAuth realm="%s"' % realm 376 if params_header: 377 auth_header = "%s, %s" % (auth_header, params_header) 378 379 return {'Authorization': auth_header}
380
381 - def to_postdata(self):
382 """Serialize as post data for a POST request.""" 383 d = {} 384 for k, v in self.iteritems(): 385 d[k.encode('utf-8')] = to_utf8_optional_iterator(v) 386 387 # tell urlencode to deal with sequence values and map them correctly 388 # to resulting querystring. for example self["k"] = ["v1", "v2"] will 389 # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D 390 return urllib.urlencode(d, True).replace('+', '%20')
391
392 - def to_url(self):
393 """Serialize as a URL for a GET request.""" 394 base_url = urlparse.urlparse(self.url) 395 try: 396 query = base_url.query 397 except AttributeError: 398 # must be python <2.5 399 query = base_url[4] 400 query = parse_qs(query) 401 for k, v in self.items(): 402 query.setdefault(k, []).append(v) 403 404 try: 405 scheme = base_url.scheme 406 netloc = base_url.netloc 407 path = base_url.path 408 params = base_url.params 409 fragment = base_url.fragment 410 except AttributeError: 411 # must be python <2.5 412 scheme = base_url[0] 413 netloc = base_url[1] 414 path = base_url[2] 415 params = base_url[3] 416 fragment = base_url[5] 417 418 url = (scheme, netloc, path, params, 419 urllib.urlencode(query, True), fragment) 420 return urlparse.urlunparse(url)
421
422 - def get_parameter(self, parameter):
423 ret = self.get(parameter) 424 if ret is None: 425 raise Error('Parameter not found: %s' % parameter) 426 427 return ret
428
430 """Return a string that contains the parameters that must be signed.""" 431 items = [] 432 for key, value in self.iteritems(): 433 if key == 'oauth_signature': 434 continue 435 # 1.0a/9.1.1 states that kvp must be sorted by key, then by value, 436 # so we unpack sequence values into multiple items for sorting. 437 if isinstance(value, basestring): 438 items.append((to_utf8_if_string(key), to_utf8(value))) 439 else: 440 try: 441 value = list(value) 442 except TypeError, e: 443 assert 'is not iterable' in str(e) 444 items.append((to_utf8_if_string(key), to_utf8_if_string(value))) 445 else: 446 items.extend((to_utf8_if_string(key), to_utf8_if_string(item)) for item in value) 447 448 # Include any query string parameters from the provided URL 449 query = urlparse.urlparse(self.url)[4] 450 451 url_items = self._split_url_string(query).items() 452 url_items = [(to_utf8(k), to_utf8(v)) for k, v in url_items if k != 'oauth_signature' ] 453 items.extend(url_items) 454 455 items.sort() 456 encoded_str = urllib.urlencode(items) 457 # Encode signature parameters per Oauth Core 1.0 protocol 458 # spec draft 7, section 3.6 459 # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6) 460 # Spaces must be encoded with "%20" instead of "+" 461 return encoded_str.replace('+', '%20').replace('%7E', '~')
462
463 - def sign_request(self, signature_method, consumer, token):
464 """Set the signature parameter to the result of sign.""" 465 466 if not self.is_form_encoded: 467 # according to 468 # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html 469 # section 4.1.1 "OAuth Consumers MUST NOT include an 470 # oauth_body_hash parameter on requests with form-encoded 471 # request bodies." 472 self['oauth_body_hash'] = base64.b64encode(sha(self.body).digest()) 473 474 if 'oauth_consumer_key' not in self: 475 self['oauth_consumer_key'] = consumer.key 476 477 if token and 'oauth_token' not in self: 478 self['oauth_token'] = token.key 479 480 self['oauth_signature_method'] = signature_method.name 481 self['oauth_signature'] = signature_method.sign(self, consumer, token)
482 483 @classmethod
484 - def make_timestamp(cls):
485 """Get seconds since epoch (UTC).""" 486 return str(int(time.time()))
487 488 @classmethod
489 - def make_nonce(cls):
490 """Generate pseudorandom number.""" 491 return str(random.randint(0, 100000000))
492 493 @classmethod
494 - def from_request(cls, http_method, http_url, headers=None, parameters=None, 495 query_string=None):
496 """Combines multiple parameter sources.""" 497 if parameters is None: 498 parameters = {} 499 500 # Headers 501 if headers and 'Authorization' in headers: 502 auth_header = headers['Authorization'] 503 # Check that the authorization header is OAuth. 504 if auth_header[:6] == 'OAuth ': 505 auth_header = auth_header[6:] 506 try: 507 # Get the parameters from the header. 508 header_params = cls._split_header(auth_header) 509 parameters.update(header_params) 510 except: 511 raise Error('Unable to parse OAuth parameters from ' 512 'Authorization header.') 513 514 # GET or POST query string. 515 if query_string: 516 query_params = cls._split_url_string(query_string) 517 parameters.update(query_params) 518 519 # URL parameters. 520 param_str = urlparse.urlparse(http_url)[4] # query 521 url_params = cls._split_url_string(param_str) 522 parameters.update(url_params) 523 524 if parameters: 525 return cls(http_method, http_url, parameters) 526 527 return None
528 529 @classmethod
530 - def from_consumer_and_token(cls, consumer, token=None, 531 http_method=HTTP_METHOD, http_url=None, parameters=None, 532 body='', is_form_encoded=False):
533 if not parameters: 534 parameters = {} 535 536 defaults = { 537 'oauth_consumer_key': consumer.key, 538 'oauth_timestamp': cls.make_timestamp(), 539 'oauth_nonce': cls.make_nonce(), 540 'oauth_version': cls.version, 541 } 542 543 defaults.update(parameters) 544 parameters = defaults 545 546 if token: 547 parameters['oauth_token'] = token.key 548 if token.verifier: 549 parameters['oauth_verifier'] = token.verifier 550 551 return Request(http_method, http_url, parameters, body=body, 552 is_form_encoded=is_form_encoded)
553 554 @classmethod
555 - def from_token_and_callback(cls, token, callback=None, 556 http_method=HTTP_METHOD, http_url=None, parameters=None):
557 558 if not parameters: 559 parameters = {} 560 561 parameters['oauth_token'] = token.key 562 563 if callback: 564 parameters['oauth_callback'] = callback 565 566 return cls(http_method, http_url, parameters)
567 568 @staticmethod
569 - def _split_header(header):
570 """Turn Authorization: header into parameters.""" 571 params = {} 572 parts = header.split(',') 573 for param in parts: 574 # Ignore realm parameter. 575 if param.find('realm') > -1: 576 continue 577 # Remove whitespace. 578 param = param.strip() 579 # Split key-value. 580 param_parts = param.split('=', 1) 581 # Remove quotes and unescape the value. 582 params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) 583 return params
584 585 @staticmethod
586 - def _split_url_string(param_str):
587 """Turn URL string into parameters.""" 588 parameters = parse_qs(param_str.encode('utf-8'), keep_blank_values=True) 589 for k, v in parameters.iteritems(): 590 parameters[k] = urllib.unquote(v[0]) 591 return parameters
592
593 594 -class SignatureMethod(object):
595 """A way of signing requests. 596 597 The OAuth protocol lets consumers and service providers pick a way to sign 598 requests. This interface shows the methods expected by the other `oauth` 599 modules for signing requests. Subclass it and implement its methods to 600 provide a new way to sign requests. 601 """ 602
603 - def signing_base(self, request, consumer, token):
604 """Calculates the string that needs to be signed. 605 606 This method returns a 2-tuple containing the starting key for the 607 signing and the message to be signed. The latter may be used in error 608 messages to help clients debug their software. 609 610 """ 611 raise NotImplementedError
612
613 - def sign(self, request, consumer, token):
614 """Returns the signature for the given request, based on the consumer 615 and token also provided. 616 617 You should use your implementation of `signing_base()` to build the 618 message to sign. Otherwise it may be less useful for debugging. 619 620 """ 621 raise NotImplementedError
622
623 - def check(self, request, consumer, token, signature):
624 """Returns whether the given signature is the correct signature for 625 the given consumer and token signing the given request.""" 626 built = self.sign(request, consumer, token) 627 return built == signature
628
629 630 -class SignatureMethod_HMAC_SHA1(SignatureMethod):
631 name = 'HMAC-SHA1' 632
633 - def signing_base(self, request, consumer, token):
634 if not hasattr(request, 'normalized_url') or request.normalized_url is None: 635 raise ValueError("Base URL for request is not set.") 636 637 sig = ( 638 escape(request.method), 639 escape(request.normalized_url), 640 escape(request.get_normalized_parameters()), 641 ) 642 643 key = '%s&' % escape(consumer.secret) 644 if token: 645 key += escape(token.secret) 646 raw = '&'.join(sig) 647 return to_bytestring(key), raw
648
649 - def sign(self, request, consumer, token):
650 """Builds the base signature string.""" 651 key, raw = self.signing_base(request, consumer, token) 652 653 hashed = hmac.new(to_bytestring(key), raw, sha) 654 655 # Calculate the digest base 64. 656 return binascii.b2a_base64(hashed.digest())[:-1]
657
658 659 -class SignatureMethod_PLAINTEXT(SignatureMethod):
660 661 name = 'PLAINTEXT' 662
663 - def signing_base(self, request, consumer, token):
664 """Concatenates the consumer key and secret with the token's 665 secret.""" 666 sig = '%s&' % escape(consumer.secret) 667 if token: 668 sig = sig + escape(token.secret) 669 return sig, sig
670
671 - def sign(self, request, consumer, token):
672 key, raw = self.signing_base(request, consumer, token) 673 return raw
674