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

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