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

Source Code for Module restkit.util.oauth2

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