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

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