Joe Gregorio | 48d361f | 2010-08-18 13:19:21 -0400 | [diff] [blame^] | 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(value) |
| 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, None, 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) |
| 329 | |
| 330 | def to_url(self): |
| 331 | """Serialize as a URL for a GET request.""" |
| 332 | base_url = urlparse.urlparse(self.url) |
| 333 | query = parse_qs(base_url.query) |
| 334 | for k, v in self.items(): |
| 335 | query.setdefault(k, []).append(v) |
| 336 | url = (base_url.scheme, base_url.netloc, base_url.path, base_url.params, |
| 337 | urllib.urlencode(query, True), base_url.fragment) |
| 338 | return urlparse.urlunparse(url) |
| 339 | |
| 340 | def get_parameter(self, parameter): |
| 341 | ret = self.get(parameter) |
| 342 | if ret is None: |
| 343 | raise Error('Parameter not found: %s' % parameter) |
| 344 | |
| 345 | return ret |
| 346 | |
| 347 | def get_normalized_parameters(self): |
| 348 | """Return a string that contains the parameters that must be signed.""" |
| 349 | items = [] |
| 350 | for key, value in self.iteritems(): |
| 351 | if key == 'oauth_signature': |
| 352 | continue |
| 353 | # 1.0a/9.1.1 states that kvp must be sorted by key, then by value, |
| 354 | # so we unpack sequence values into multiple items for sorting. |
| 355 | if hasattr(value, '__iter__'): |
| 356 | items.extend((key, item) for item in value) |
| 357 | else: |
| 358 | items.append((key, value)) |
| 359 | |
| 360 | # Include any query string parameters from the provided URL |
| 361 | query = urlparse.urlparse(self.url)[4] |
| 362 | items.extend(self._split_url_string(query).items()) |
| 363 | |
| 364 | encoded_str = urllib.urlencode(sorted(items)) |
| 365 | # Encode signature parameters per Oauth Core 1.0 protocol |
| 366 | # spec draft 7, section 3.6 |
| 367 | # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6) |
| 368 | # Spaces must be encoded with "%20" instead of "+" |
| 369 | return encoded_str.replace('+', '%20') |
| 370 | |
| 371 | def sign_request(self, signature_method, consumer, token): |
| 372 | """Set the signature parameter to the result of sign.""" |
| 373 | |
| 374 | if 'oauth_consumer_key' not in self: |
| 375 | self['oauth_consumer_key'] = consumer.key |
| 376 | |
| 377 | if token and 'oauth_token' not in self: |
| 378 | self['oauth_token'] = token.key |
| 379 | |
| 380 | self['oauth_signature_method'] = signature_method.name |
| 381 | self['oauth_signature'] = signature_method.sign(self, consumer, token) |
| 382 | |
| 383 | @classmethod |
| 384 | def make_timestamp(cls): |
| 385 | """Get seconds since epoch (UTC).""" |
| 386 | return str(int(time.time())) |
| 387 | |
| 388 | @classmethod |
| 389 | def make_nonce(cls): |
| 390 | """Generate pseudorandom number.""" |
| 391 | return str(random.randint(0, 100000000)) |
| 392 | |
| 393 | @classmethod |
| 394 | def from_request(cls, http_method, http_url, headers=None, parameters=None, |
| 395 | query_string=None): |
| 396 | """Combines multiple parameter sources.""" |
| 397 | if parameters is None: |
| 398 | parameters = {} |
| 399 | |
| 400 | # Headers |
| 401 | if headers and 'Authorization' in headers: |
| 402 | auth_header = headers['Authorization'] |
| 403 | # Check that the authorization header is OAuth. |
| 404 | if auth_header[:6] == 'OAuth ': |
| 405 | auth_header = auth_header[6:] |
| 406 | try: |
| 407 | # Get the parameters from the header. |
| 408 | header_params = cls._split_header(auth_header) |
| 409 | parameters.update(header_params) |
| 410 | except: |
| 411 | raise Error('Unable to parse OAuth parameters from ' |
| 412 | 'Authorization header.') |
| 413 | |
| 414 | # GET or POST query string. |
| 415 | if query_string: |
| 416 | query_params = cls._split_url_string(query_string) |
| 417 | parameters.update(query_params) |
| 418 | |
| 419 | # URL parameters. |
| 420 | param_str = urlparse.urlparse(http_url)[4] # query |
| 421 | url_params = cls._split_url_string(param_str) |
| 422 | parameters.update(url_params) |
| 423 | |
| 424 | if parameters: |
| 425 | return cls(http_method, http_url, parameters) |
| 426 | |
| 427 | return None |
| 428 | |
| 429 | @classmethod |
| 430 | def from_consumer_and_token(cls, consumer, token=None, |
| 431 | http_method=HTTP_METHOD, http_url=None, parameters=None): |
| 432 | if not parameters: |
| 433 | parameters = {} |
| 434 | |
| 435 | defaults = { |
| 436 | 'oauth_consumer_key': consumer.key, |
| 437 | 'oauth_timestamp': cls.make_timestamp(), |
| 438 | 'oauth_nonce': cls.make_nonce(), |
| 439 | 'oauth_version': cls.version, |
| 440 | } |
| 441 | |
| 442 | defaults.update(parameters) |
| 443 | parameters = defaults |
| 444 | |
| 445 | if token: |
| 446 | parameters['oauth_token'] = token.key |
| 447 | if token.verifier: |
| 448 | parameters['oauth_verifier'] = token.verifier |
| 449 | |
| 450 | return Request(http_method, http_url, parameters) |
| 451 | |
| 452 | @classmethod |
| 453 | def from_token_and_callback(cls, token, callback=None, |
| 454 | http_method=HTTP_METHOD, http_url=None, parameters=None): |
| 455 | |
| 456 | if not parameters: |
| 457 | parameters = {} |
| 458 | |
| 459 | parameters['oauth_token'] = token.key |
| 460 | |
| 461 | if callback: |
| 462 | parameters['oauth_callback'] = callback |
| 463 | |
| 464 | return cls(http_method, http_url, parameters) |
| 465 | |
| 466 | @staticmethod |
| 467 | def _split_header(header): |
| 468 | """Turn Authorization: header into parameters.""" |
| 469 | params = {} |
| 470 | parts = header.split(',') |
| 471 | for param in parts: |
| 472 | # Ignore realm parameter. |
| 473 | if param.find('realm') > -1: |
| 474 | continue |
| 475 | # Remove whitespace. |
| 476 | param = param.strip() |
| 477 | # Split key-value. |
| 478 | param_parts = param.split('=', 1) |
| 479 | # Remove quotes and unescape the value. |
| 480 | params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) |
| 481 | return params |
| 482 | |
| 483 | @staticmethod |
| 484 | def _split_url_string(param_str): |
| 485 | """Turn URL string into parameters.""" |
| 486 | parameters = parse_qs(param_str, keep_blank_values=False) |
| 487 | for k, v in parameters.iteritems(): |
| 488 | parameters[k] = urllib.unquote(v[0]) |
| 489 | return parameters |
| 490 | |
| 491 | |
| 492 | class Client(httplib2.Http): |
| 493 | """OAuthClient is a worker to attempt to execute a request.""" |
| 494 | |
| 495 | def __init__(self, consumer, token=None, cache=None, timeout=None, |
| 496 | proxy_info=None): |
| 497 | |
| 498 | if consumer is not None and not isinstance(consumer, Consumer): |
| 499 | raise ValueError("Invalid consumer.") |
| 500 | |
| 501 | if token is not None and not isinstance(token, Token): |
| 502 | raise ValueError("Invalid token.") |
| 503 | |
| 504 | self.consumer = consumer |
| 505 | self.token = token |
| 506 | self.method = SignatureMethod_HMAC_SHA1() |
| 507 | |
| 508 | httplib2.Http.__init__(self, cache=cache, timeout=timeout, |
| 509 | proxy_info=proxy_info) |
| 510 | |
| 511 | def set_signature_method(self, method): |
| 512 | if not isinstance(method, SignatureMethod): |
| 513 | raise ValueError("Invalid signature method.") |
| 514 | |
| 515 | self.method = method |
| 516 | |
| 517 | def request(self, uri, method="GET", body=None, headers=None, |
| 518 | redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None): |
| 519 | DEFAULT_CONTENT_TYPE = 'application/x-www-form-urlencoded' |
| 520 | |
| 521 | if not isinstance(headers, dict): |
| 522 | headers = {} |
| 523 | |
| 524 | is_multipart = method == 'POST' and headers.get('Content-Type', |
| 525 | DEFAULT_CONTENT_TYPE) != DEFAULT_CONTENT_TYPE |
| 526 | |
| 527 | if body and method == "POST" and not is_multipart: |
| 528 | parameters = dict(parse_qsl(body)) |
| 529 | else: |
| 530 | parameters = None |
| 531 | |
| 532 | req = Request.from_consumer_and_token(self.consumer, |
| 533 | token=self.token, http_method=method, http_url=uri, |
| 534 | parameters=parameters) |
| 535 | |
| 536 | req.sign_request(self.method, self.consumer, self.token) |
| 537 | |
| 538 | if method == "POST": |
| 539 | headers['Content-Type'] = headers.get('Content-Type', |
| 540 | DEFAULT_CONTENT_TYPE) |
| 541 | if is_multipart: |
| 542 | headers.update(req.to_header()) |
| 543 | else: |
| 544 | body = req.to_postdata() |
| 545 | elif method == "GET": |
| 546 | uri = req.to_url() |
| 547 | else: |
| 548 | headers.update(req.to_header()) |
| 549 | |
| 550 | return httplib2.Http.request(self, uri, method=method, body=body, |
| 551 | headers=headers, redirections=redirections, |
| 552 | connection_type=connection_type) |
| 553 | |
| 554 | |
| 555 | class Server(object): |
| 556 | """A skeletal implementation of a service provider, providing protected |
| 557 | resources to requests from authorized consumers. |
| 558 | |
| 559 | This class implements the logic to check requests for authorization. You |
| 560 | can use it with your web server or web framework to protect certain |
| 561 | resources with OAuth. |
| 562 | """ |
| 563 | |
| 564 | timestamp_threshold = 300 # In seconds, five minutes. |
| 565 | version = VERSION |
| 566 | signature_methods = None |
| 567 | |
| 568 | def __init__(self, signature_methods=None): |
| 569 | self.signature_methods = signature_methods or {} |
| 570 | |
| 571 | def add_signature_method(self, signature_method): |
| 572 | self.signature_methods[signature_method.name] = signature_method |
| 573 | return self.signature_methods |
| 574 | |
| 575 | def verify_request(self, request, consumer, token): |
| 576 | """Verifies an api call and checks all the parameters.""" |
| 577 | |
| 578 | version = self._get_version(request) |
| 579 | self._check_signature(request, consumer, token) |
| 580 | parameters = request.get_nonoauth_parameters() |
| 581 | return parameters |
| 582 | |
| 583 | def build_authenticate_header(self, realm=''): |
| 584 | """Optional support for the authenticate header.""" |
| 585 | return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} |
| 586 | |
| 587 | def _get_version(self, request): |
| 588 | """Verify the correct version request for this server.""" |
| 589 | try: |
| 590 | version = request.get_parameter('oauth_version') |
| 591 | except: |
| 592 | version = VERSION |
| 593 | |
| 594 | if version and version != self.version: |
| 595 | raise Error('OAuth version %s not supported.' % str(version)) |
| 596 | |
| 597 | return version |
| 598 | |
| 599 | def _get_signature_method(self, request): |
| 600 | """Figure out the signature with some defaults.""" |
| 601 | try: |
| 602 | signature_method = request.get_parameter('oauth_signature_method') |
| 603 | except: |
| 604 | signature_method = SIGNATURE_METHOD |
| 605 | |
| 606 | try: |
| 607 | # Get the signature method object. |
| 608 | signature_method = self.signature_methods[signature_method] |
| 609 | except: |
| 610 | signature_method_names = ', '.join(self.signature_methods.keys()) |
| 611 | raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) |
| 612 | |
| 613 | return signature_method |
| 614 | |
| 615 | def _get_verifier(self, request): |
| 616 | return request.get_parameter('oauth_verifier') |
| 617 | |
| 618 | def _check_signature(self, request, consumer, token): |
| 619 | timestamp, nonce = request._get_timestamp_nonce() |
| 620 | self._check_timestamp(timestamp) |
| 621 | signature_method = self._get_signature_method(request) |
| 622 | |
| 623 | try: |
| 624 | signature = request.get_parameter('oauth_signature') |
| 625 | except: |
| 626 | raise MissingSignature('Missing oauth_signature.') |
| 627 | |
| 628 | # Validate the signature. |
| 629 | valid = signature_method.check(request, consumer, token, signature) |
| 630 | |
| 631 | if not valid: |
| 632 | key, base = signature_method.signing_base(request, consumer, token) |
| 633 | |
| 634 | raise Error('Invalid signature. Expected signature base ' |
| 635 | 'string: %s' % base) |
| 636 | |
| 637 | built = signature_method.sign(request, consumer, token) |
| 638 | |
| 639 | def _check_timestamp(self, timestamp): |
| 640 | """Verify that timestamp is recentish.""" |
| 641 | timestamp = int(timestamp) |
| 642 | now = int(time.time()) |
| 643 | lapsed = now - timestamp |
| 644 | if lapsed > self.timestamp_threshold: |
| 645 | raise Error('Expired timestamp: given %d and now %s has a ' |
| 646 | 'greater difference than threshold %d' % (timestamp, now, |
| 647 | self.timestamp_threshold)) |
| 648 | |
| 649 | |
| 650 | class SignatureMethod(object): |
| 651 | """A way of signing requests. |
| 652 | |
| 653 | The OAuth protocol lets consumers and service providers pick a way to sign |
| 654 | requests. This interface shows the methods expected by the other `oauth` |
| 655 | modules for signing requests. Subclass it and implement its methods to |
| 656 | provide a new way to sign requests. |
| 657 | """ |
| 658 | |
| 659 | def signing_base(self, request, consumer, token): |
| 660 | """Calculates the string that needs to be signed. |
| 661 | |
| 662 | This method returns a 2-tuple containing the starting key for the |
| 663 | signing and the message to be signed. The latter may be used in error |
| 664 | messages to help clients debug their software. |
| 665 | |
| 666 | """ |
| 667 | raise NotImplementedError |
| 668 | |
| 669 | def sign(self, request, consumer, token): |
| 670 | """Returns the signature for the given request, based on the consumer |
| 671 | and token also provided. |
| 672 | |
| 673 | You should use your implementation of `signing_base()` to build the |
| 674 | message to sign. Otherwise it may be less useful for debugging. |
| 675 | |
| 676 | """ |
| 677 | raise NotImplementedError |
| 678 | |
| 679 | def check(self, request, consumer, token, signature): |
| 680 | """Returns whether the given signature is the correct signature for |
| 681 | the given consumer and token signing the given request.""" |
| 682 | built = self.sign(request, consumer, token) |
| 683 | return built == signature |
| 684 | |
| 685 | |
| 686 | class SignatureMethod_HMAC_SHA1(SignatureMethod): |
| 687 | name = 'HMAC-SHA1' |
| 688 | |
| 689 | def signing_base(self, request, consumer, token): |
| 690 | if request.normalized_url is None: |
| 691 | raise ValueError("Base URL for request is not set.") |
| 692 | |
| 693 | sig = ( |
| 694 | escape(request.method), |
| 695 | escape(request.normalized_url), |
| 696 | escape(request.get_normalized_parameters()), |
| 697 | ) |
| 698 | |
| 699 | key = '%s&' % escape(consumer.secret) |
| 700 | if token: |
| 701 | key += escape(token.secret) |
| 702 | raw = '&'.join(sig) |
| 703 | return key, raw |
| 704 | |
| 705 | def sign(self, request, consumer, token): |
| 706 | """Builds the base signature string.""" |
| 707 | key, raw = self.signing_base(request, consumer, token) |
| 708 | |
| 709 | # HMAC object. |
| 710 | try: |
| 711 | from hashlib import sha1 as sha |
| 712 | except ImportError: |
| 713 | import sha # Deprecated |
| 714 | |
| 715 | hashed = hmac.new(key, raw, sha) |
| 716 | |
| 717 | # Calculate the digest base 64. |
| 718 | return binascii.b2a_base64(hashed.digest())[:-1] |
| 719 | |
| 720 | |
| 721 | class SignatureMethod_PLAINTEXT(SignatureMethod): |
| 722 | |
| 723 | name = 'PLAINTEXT' |
| 724 | |
| 725 | def signing_base(self, request, consumer, token): |
| 726 | """Concatenates the consumer key and secret with the token's |
| 727 | secret.""" |
| 728 | sig = '%s&' % escape(consumer.secret) |
| 729 | if token: |
| 730 | sig = sig + escape(token.secret) |
| 731 | return sig, sig |
| 732 | |
| 733 | def sign(self, request, consumer, token): |
| 734 | key, raw = self.signing_base(request, consumer, token) |
| 735 | return raw |