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