4 Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
 
   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:
 
  13 The above copyright notice and this permission notice shall be included in
 
  14 all copies or substantial portions of the Software.
 
  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
 
  35     from urlparse import parse_qs
 
  36     parse_qs # placate pyflakes
 
  38     # fall back for Python 2.5
 
  39     from cgi import parse_qs
 
  42     from hashlib import sha1
 
  45     # hashlib was added in Python 2.5
 
  50 __version__ = _version.__version__
 
  52 OAUTH_VERSION = '1.0'  # Hi Blaine!
 
  54 SIGNATURE_METHOD = 'PLAINTEXT'
 
  57 class Error(RuntimeError):
 
  58     """Generic exception class."""
 
  60     def __init__(self, message='OAuth error occurred.'):
 
  61         self._message = message
 
  65         """A hack to get around the deprecation errors in 2.6."""
 
  72 class MissingSignature(Error):
 
  76 def build_authenticate_header(realm=''):
 
  77     """Optional WWW-Authenticate header (401 error)"""
 
  78     return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
 
  81 def build_xoauth_string(url, consumer, token=None):
 
  82     """Build an XOAUTH string for use in SMTP/IMPA authentication."""
 
  83     request = Request.from_consumer_and_token(consumer, token,
 
  86     signing_method = SignatureMethod_HMAC_SHA1()
 
  87     request.sign_request(signing_method, consumer, token)
 
  90     for k, v in sorted(request.iteritems()):
 
  92             params.append('%s="%s"' % (k, escape(v)))
 
  94     return "%s %s %s" % ("GET", url, ','.join(params))
 
  98     """ Convert to unicode, raise exception with instructive error
 
  99     message if s is not unicode, ascii, or utf-8. """
 
 100     if not isinstance(s, unicode):
 
 101         if not isinstance(s, str):
 
 102             raise TypeError('You are required to pass either unicode or string here, not: %r (%s)' % (type(s), s))
 
 104             s = s.decode('utf-8')
 
 105         except UnicodeDecodeError, le:
 
 106             raise TypeError('You are required to pass either a unicode object or a utf-8 string here. You passed a Python string object which contained non-utf-8: %r. The UnicodeDecodeError that resulted from attempting to interpret it as utf-8 was: %s' % (s, le,))
 
 110     return to_unicode(s).encode('utf-8')
 
 112 def to_unicode_if_string(s):
 
 113     if isinstance(s, basestring):
 
 118 def to_utf8_if_string(s):
 
 119     if isinstance(s, basestring):
 
 124 def to_unicode_optional_iterator(x):
 
 126     Raise TypeError if x is a str containing non-utf8 bytes or if x is
 
 127     an iterable which contains such a str.
 
 129     if isinstance(x, basestring):
 
 135         assert 'is not iterable' in str(e)
 
 138         return [ to_unicode(e) for e in l ]
 
 140 def to_utf8_optional_iterator(x):
 
 142     Raise TypeError if x is a str or if x is an iterable which
 
 145     if isinstance(x, basestring):
 
 151         assert 'is not iterable' in str(e)
 
 154         return [ to_utf8_if_string(e) for e in l ]
 
 157     """Escape a URL including any /."""
 
 158     return urllib.quote(s.encode('utf-8'), safe='~')
 
 160 def generate_timestamp():
 
 161     """Get seconds since epoch (UTC)."""
 
 162     return int(time.time())
 
 165 def generate_nonce(length=8):
 
 166     """Generate pseudorandom number."""
 
 167     return ''.join([str(random.randint(0, 9)) for i in range(length)])
 
 170 def generate_verifier(length=8):
 
 171     """Generate pseudorandom number."""
 
 172     return ''.join([str(random.randint(0, 9)) for i in range(length)])
 
 175 class Consumer(object):
 
 176     """A consumer of OAuth-protected services.
 
 178     The OAuth consumer is a "third-party" service that wants to access
 
 179     protected resources from an OAuth service provider on behalf of an end
 
 180     user. It's kind of the OAuth client.
 
 182     Usually a consumer must be registered with the service provider by the
 
 183     developer of the consumer software. As part of that process, the service
 
 184     provider gives the consumer a *key* and a *secret* with which the consumer
 
 185     software can identify itself to the service. The consumer will include its
 
 186     key in each request to identify itself, but will use its secret only when
 
 187     signing requests, to prove that the request is from that particular
 
 190     Once registered, the consumer can then use its consumer credentials to ask
 
 191     the service provider for a request token, kicking off the OAuth
 
 192     authorization process.
 
 198     def __init__(self, key, secret):
 
 202         if self.key is None or self.secret is None:
 
 203             raise ValueError("Key and secret must be set.")
 
 206         data = {'oauth_consumer_key': self.key,
 
 207             'oauth_consumer_secret': self.secret}
 
 209         return urllib.urlencode(data)
 
 213     """An OAuth credential used to request authorization or a protected
 
 216     Tokens in OAuth comprise a *key* and a *secret*. The key is included in
 
 217     requests to identify the token being used, but the secret is used only in
 
 218     the signature, to prove that the requester is who the server gave the
 
 221     When first negotiating the authorization, the consumer asks for a *request
 
 222     token* that the live user authorizes with the service provider. The
 
 223     consumer then exchanges the request token for an *access token* that can
 
 224     be used to access protected resources.
 
 230     callback_confirmed = None
 
 233     def __init__(self, key, secret):
 
 237         if self.key is None or self.secret is None:
 
 238             raise ValueError("Key and secret must be set.")
 
 240     def set_callback(self, callback):
 
 241         self.callback = callback
 
 242         self.callback_confirmed = 'true'
 
 244     def set_verifier(self, verifier=None):
 
 245         if verifier is not None:
 
 246             self.verifier = verifier
 
 248             self.verifier = generate_verifier()
 
 250     def get_callback_url(self):
 
 251         if self.callback and self.verifier:
 
 252             # Append the oauth_verifier.
 
 253             parts = urlparse.urlparse(self.callback)
 
 254             scheme, netloc, path, params, query, fragment = parts[:6]
 
 256                 query = '%s&oauth_verifier=%s' % (query, self.verifier)
 
 258                 query = 'oauth_verifier=%s' % self.verifier
 
 259             return urlparse.urlunparse((scheme, netloc, path, params,
 
 264         """Returns this token as a plain string, suitable for storage.
 
 266         The resulting string includes the token's secret, so you should never
 
 267         send or store this string where a third party can read it.
 
 271             'oauth_token': self.key,
 
 272             'oauth_token_secret': self.secret,
 
 275         if self.callback_confirmed is not None:
 
 276             data['oauth_callback_confirmed'] = self.callback_confirmed
 
 277         return urllib.urlencode(data)
 
 281         """Deserializes a token from a string like one returned by
 
 285             raise ValueError("Invalid parameter string.")
 
 287         params = parse_qs(s, keep_blank_values=False)
 
 289             raise ValueError("Invalid parameter string.")
 
 292             key = params['oauth_token'][0]
 
 294             raise ValueError("'oauth_token' not found in OAuth request.")
 
 297             secret = params['oauth_token_secret'][0]
 
 299             raise ValueError("'oauth_token_secret' not found in " 
 
 302         token = Token(key, secret)
 
 304             token.callback_confirmed = params['oauth_callback_confirmed'][0]
 
 306             pass  # 1.0, no callback confirmed.
 
 310         return self.to_string()
 
 318             return self.__dict__[name]
 
 320             raise AttributeError(name)
 
 323         del self.__dict__[name]
 
 325     return property(getter, attr, deleter)
 
 330     """The parameters and information for an HTTP request, suitable for
 
 331     authorizing with OAuth credentials.
 
 333     When a consumer wants to access a service's protected resources, it does
 
 334     so using a signed HTTP request identifying itself (the consumer) with its
 
 335     key, and providing an access token authorized by the end user to access
 
 340     version = OAUTH_VERSION
 
 342     def __init__(self, method=HTTP_METHOD, url=None, parameters=None,
 
 343                  body='', is_form_encoded=False):
 
 345             self.url = to_unicode(url)
 
 347         if parameters is not None:
 
 348             for k, v in parameters.iteritems():
 
 350                 v = to_unicode_optional_iterator(v)
 
 353         self.is_form_encoded = is_form_encoded
 
 357     def url(self, value):
 
 358         self.__dict__['url'] = value
 
 359         if value is not None:
 
 360             scheme, netloc, path, params, query, fragment = urlparse.urlparse(value)
 
 362             # Exclude default port numbers.
 
 363             if scheme == 'http' and netloc[-3:] == ':80':
 
 365             elif scheme == 'https' and netloc[-4:] == ':443':
 
 367             if scheme not in ('http', 'https'):
 
 368                 raise ValueError("Unsupported URL %s (%s)." % (value, scheme))
 
 370             # Normalized URL excludes params, query, and fragment.
 
 371             self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None))
 
 373             self.normalized_url = None
 
 374             self.__dict__['url'] = None
 
 377     def method(self, value):
 
 378         self.__dict__['method'] = value.upper()
 
 380     def _get_timestamp_nonce(self):
 
 381         return self['oauth_timestamp'], self['oauth_nonce']
 
 383     def get_nonoauth_parameters(self):
 
 384         """Get any non-OAuth parameters."""
 
 385         return dict([(k, v) for k, v in self.iteritems() 
 
 386                     if not k.startswith('oauth_')])
 
 388     def to_header(self, realm=''):
 
 389         """Serialize as a header for an HTTPAuth request."""
 
 390         oauth_params = ((k, v) for k, v in self.items() 
 
 391                             if k.startswith('oauth_'))
 
 392         stringy_params = ((k, escape(str(v))) for k, v in oauth_params)
 
 393         header_params = ('%s="%s"' % (k, v) for k, v in stringy_params)
 
 394         params_header = ', '.join(header_params)
 
 396         auth_header = 'OAuth realm="%s"' % realm
 
 398             auth_header = "%s, %s" % (auth_header, params_header)
 
 400         return {'Authorization': auth_header}
 
 402     def to_postdata(self):
 
 403         """Serialize as post data for a POST request."""
 
 405         for k, v in self.iteritems():
 
 406             d[k.encode('utf-8')] = to_utf8_optional_iterator(v)
 
 408         # tell urlencode to deal with sequence values and map them correctly
 
 409         # to resulting querystring. for example self["k"] = ["v1", "v2"] will
 
 410         # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D
 
 411         return urllib.urlencode(d, True).replace('+', '%20')
 
 414         """Serialize as a URL for a GET request."""
 
 415         base_url = urlparse.urlparse(self.url)
 
 417             query = base_url.query
 
 418         except AttributeError:
 
 419             # must be python <2.5
 
 421         query = parse_qs(query)
 
 422         for k, v in self.items():
 
 423             query.setdefault(k, []).append(v)
 
 426             scheme = base_url.scheme
 
 427             netloc = base_url.netloc
 
 429             params = base_url.params
 
 430             fragment = base_url.fragment
 
 431         except AttributeError:
 
 432             # must be python <2.5
 
 437             fragment = base_url[5]
 
 439         url = (scheme, netloc, path, params,
 
 440                urllib.urlencode(query, True), fragment)
 
 441         return urlparse.urlunparse(url)
 
 443     def get_parameter(self, parameter):
 
 444         ret = self.get(parameter)
 
 446             raise Error('Parameter not found: %s' % parameter)
 
 450     def get_normalized_parameters(self):
 
 451         """Return a string that contains the parameters that must be signed."""
 
 453         for key, value in self.iteritems():
 
 454             if key == 'oauth_signature':
 
 456             # 1.0a/9.1.1 states that kvp must be sorted by key, then by value,
 
 457             # so we unpack sequence values into multiple items for sorting.
 
 458             if isinstance(value, basestring):
 
 459                 items.append((to_utf8_if_string(key), to_utf8(value)))
 
 464                     assert 'is not iterable' in str(e)
 
 465                     items.append((to_utf8_if_string(key), to_utf8_if_string(value)))
 
 467                     items.extend((to_utf8_if_string(key), to_utf8_if_string(item)) for item in value)
 
 469         # Include any query string parameters from the provided URL
 
 470         query = urlparse.urlparse(self.url)[4]
 
 472         url_items = self._split_url_string(query).items()
 
 473         url_items = [(to_utf8(k), to_utf8(v)) for k, v in url_items if k != 'oauth_signature' ]
 
 474         items.extend(url_items)
 
 477         encoded_str = urllib.urlencode(items)
 
 478         # Encode signature parameters per Oauth Core 1.0 protocol
 
 479         # spec draft 7, section 3.6
 
 480         # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6)
 
 481         # Spaces must be encoded with "%20" instead of "+"
 
 482         return encoded_str.replace('+', '%20').replace('%7E', '~')
 
 484     def sign_request(self, signature_method, consumer, token):
 
 485         """Set the signature parameter to the result of sign."""
 
 487         if not self.is_form_encoded:
 
 489             # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
 
 490             # section 4.1.1 "OAuth Consumers MUST NOT include an
 
 491             # oauth_body_hash parameter on requests with form-encoded
 
 493             self['oauth_body_hash'] = base64.b64encode(sha(self.body).digest())
 
 495         if 'oauth_consumer_key' not in self:
 
 496             self['oauth_consumer_key'] = consumer.key
 
 498         if token and 'oauth_token' not in self:
 
 499             self['oauth_token'] = token.key
 
 501         self['oauth_signature_method'] = signature_method.name
 
 502         self['oauth_signature'] = signature_method.sign(self, consumer, token)
 
 505     def make_timestamp(cls):
 
 506         """Get seconds since epoch (UTC)."""
 
 507         return str(int(time.time()))
 
 511         """Generate pseudorandom number."""
 
 512         return str(random.randint(0, 100000000))
 
 515     def from_request(cls, http_method, http_url, headers=None, parameters=None,
 
 517         """Combines multiple parameter sources."""
 
 518         if parameters is None:
 
 522         if headers and 'Authorization' in headers:
 
 523             auth_header = headers['Authorization']
 
 524             # Check that the authorization header is OAuth.
 
 525             if auth_header[:6] == 'OAuth ':
 
 526                 auth_header = auth_header[6:]
 
 528                     # Get the parameters from the header.
 
 529                     header_params = cls._split_header(auth_header)
 
 530                     parameters.update(header_params)
 
 532                     raise Error('Unable to parse OAuth parameters from '
 
 533                         'Authorization header.')
 
 535         # GET or POST query string.
 
 537             query_params = cls._split_url_string(query_string)
 
 538             parameters.update(query_params)
 
 541         param_str = urlparse.urlparse(http_url)[4] # query
 
 542         url_params = cls._split_url_string(param_str)
 
 543         parameters.update(url_params)
 
 546             return cls(http_method, http_url, parameters)
 
 551     def from_consumer_and_token(cls, consumer, token=None,
 
 552             http_method=HTTP_METHOD, http_url=None, parameters=None,
 
 553             body='', is_form_encoded=False):
 
 558             'oauth_consumer_key': consumer.key,
 
 559             'oauth_timestamp': cls.make_timestamp(),
 
 560             'oauth_nonce': cls.make_nonce(),
 
 561             'oauth_version': cls.version,
 
 564         defaults.update(parameters)
 
 565         parameters = defaults
 
 568             parameters['oauth_token'] = token.key
 
 570                 parameters['oauth_verifier'] = token.verifier
 
 572         return Request(http_method, http_url, parameters, body=body, 
 
 573                        is_form_encoded=is_form_encoded)
 
 576     def from_token_and_callback(cls, token, callback=None, 
 
 577         http_method=HTTP_METHOD, http_url=None, parameters=None):
 
 582         parameters['oauth_token'] = token.key
 
 585             parameters['oauth_callback'] = callback
 
 587         return cls(http_method, http_url, parameters)
 
 590     def _split_header(header):
 
 591         """Turn Authorization: header into parameters."""
 
 593         parts = header.split(',')
 
 595             # Ignore realm parameter.
 
 596             if param.find('realm') > -1:
 
 599             param = param.strip()
 
 601             param_parts = param.split('=', 1)
 
 602             # Remove quotes and unescape the value.
 
 603             params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
 
 607     def _split_url_string(param_str):
 
 608         """Turn URL string into parameters."""
 
 609         parameters = parse_qs(param_str.encode('utf-8'), keep_blank_values=True)
 
 610         for k, v in parameters.iteritems():
 
 611             parameters[k] = urllib.unquote(v[0])
 
 615 class Client(httplib2.Http):
 
 616     """OAuthClient is a worker to attempt to execute a request."""
 
 618     def __init__(self, consumer, token=None, cache=None, timeout=None,
 
 621         if consumer is not None and not isinstance(consumer, Consumer):
 
 622             raise ValueError("Invalid consumer.")
 
 624         if token is not None and not isinstance(token, Token):
 
 625             raise ValueError("Invalid token.")
 
 627         self.consumer = consumer
 
 629         self.method = SignatureMethod_HMAC_SHA1()
 
 631         httplib2.Http.__init__(self, cache=cache, timeout=timeout, proxy_info=proxy_info)
 
 633     def set_signature_method(self, method):
 
 634         if not isinstance(method, SignatureMethod):
 
 635             raise ValueError("Invalid signature method.")
 
 639     def request(self, uri, method="GET", body='', headers=None, 
 
 640         redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None):
 
 641         DEFAULT_POST_CONTENT_TYPE = 'application/x-www-form-urlencoded'
 
 643         if not isinstance(headers, dict):
 
 647             headers['Content-Type'] = headers.get('Content-Type', 
 
 648                 DEFAULT_POST_CONTENT_TYPE)
 
 651             headers.get('Content-Type') == 'application/x-www-form-urlencoded'
 
 653         if is_form_encoded and body:
 
 654             parameters = parse_qs(body)
 
 658         req = Request.from_consumer_and_token(self.consumer, 
 
 659             token=self.token, http_method=method, http_url=uri, 
 
 660             parameters=parameters, body=body, is_form_encoded=is_form_encoded)
 
 662         req.sign_request(self.method, self.consumer, self.token)
 
 664         schema, rest = urllib.splittype(uri)
 
 665         if rest.startswith('//'):
 
 669         host, rest = urllib.splithost(rest)
 
 671         realm = schema + ':' + hierpart + host
 
 674             body = req.to_postdata()
 
 675         elif method == "GET":
 
 678             headers.update(req.to_header(realm=realm))
 
 680         return httplib2.Http.request(self, uri, method=method, body=body,
 
 681             headers=headers, redirections=redirections,
 
 682             connection_type=connection_type)
 
 685 class Server(object):
 
 686     """A skeletal implementation of a service provider, providing protected
 
 687     resources to requests from authorized consumers.
 
 689     This class implements the logic to check requests for authorization. You
 
 690     can use it with your web server or web framework to protect certain
 
 691     resources with OAuth.
 
 694     timestamp_threshold = 300 # In seconds, five minutes.
 
 695     version = OAUTH_VERSION
 
 696     signature_methods = None
 
 698     def __init__(self, signature_methods=None):
 
 699         self.signature_methods = signature_methods or {}
 
 701     def add_signature_method(self, signature_method):
 
 702         self.signature_methods[signature_method.name] = signature_method
 
 703         return self.signature_methods
 
 705     def verify_request(self, request, consumer, token):
 
 706         """Verifies an api call and checks all the parameters."""
 
 708         self._check_version(request)
 
 709         self._check_signature(request, consumer, token)
 
 710         parameters = request.get_nonoauth_parameters()
 
 713     def build_authenticate_header(self, realm=''):
 
 714         """Optional support for the authenticate header."""
 
 715         return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
 
 717     def _check_version(self, request):
 
 718         """Verify the correct version of the request for this server."""
 
 719         version = self._get_version(request)
 
 720         if version and version != self.version:
 
 721             raise Error('OAuth version %s not supported.' % str(version))
 
 723     def _get_version(self, request):
 
 724         """Return the version of the request for this server."""
 
 726             version = request.get_parameter('oauth_version')
 
 728             version = OAUTH_VERSION
 
 732     def _get_signature_method(self, request):
 
 733         """Figure out the signature with some defaults."""
 
 735             signature_method = request.get_parameter('oauth_signature_method')
 
 737             signature_method = SIGNATURE_METHOD
 
 740             # Get the signature method object.
 
 741             signature_method = self.signature_methods[signature_method]
 
 743             signature_method_names = ', '.join(self.signature_methods.keys())
 
 744             raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
 
 746         return signature_method
 
 748     def _get_verifier(self, request):
 
 749         return request.get_parameter('oauth_verifier')
 
 751     def _check_signature(self, request, consumer, token):
 
 752         timestamp, nonce = request._get_timestamp_nonce()
 
 753         self._check_timestamp(timestamp)
 
 754         signature_method = self._get_signature_method(request)
 
 757             signature = request.get_parameter('oauth_signature')
 
 759             raise MissingSignature('Missing oauth_signature.')
 
 761         # Validate the signature.
 
 762         valid = signature_method.check(request, consumer, token, signature)
 
 765             key, base = signature_method.signing_base(request, consumer, token)
 
 767             raise Error('Invalid signature. Expected signature base ' 
 
 770     def _check_timestamp(self, timestamp):
 
 771         """Verify that timestamp is recentish."""
 
 772         timestamp = int(timestamp)
 
 773         now = int(time.time())
 
 774         lapsed = now - timestamp
 
 775         if lapsed > self.timestamp_threshold:
 
 776             raise Error('Expired timestamp: given %d and now %s has a '
 
 777                 'greater difference than threshold %d' % (timestamp, now, 
 
 778                     self.timestamp_threshold))
 
 781 class SignatureMethod(object):
 
 782     """A way of signing requests.
 
 784     The OAuth protocol lets consumers and service providers pick a way to sign
 
 785     requests. This interface shows the methods expected by the other `oauth`
 
 786     modules for signing requests. Subclass it and implement its methods to
 
 787     provide a new way to sign requests.
 
 790     def signing_base(self, request, consumer, token):
 
 791         """Calculates the string that needs to be signed.
 
 793         This method returns a 2-tuple containing the starting key for the
 
 794         signing and the message to be signed. The latter may be used in error
 
 795         messages to help clients debug their software.
 
 798         raise NotImplementedError
 
 800     def sign(self, request, consumer, token):
 
 801         """Returns the signature for the given request, based on the consumer
 
 802         and token also provided.
 
 804         You should use your implementation of `signing_base()` to build the
 
 805         message to sign. Otherwise it may be less useful for debugging.
 
 808         raise NotImplementedError
 
 810     def check(self, request, consumer, token, signature):
 
 811         """Returns whether the given signature is the correct signature for
 
 812         the given consumer and token signing the given request."""
 
 813         built = self.sign(request, consumer, token)
 
 814         return built == signature
 
 817 class SignatureMethod_HMAC_SHA1(SignatureMethod):
 
 820     def signing_base(self, request, consumer, token):
 
 821         if not hasattr(request, 'normalized_url') or request.normalized_url is None:
 
 822             raise ValueError("Base URL for request is not set.")
 
 825             escape(request.method),
 
 826             escape(request.normalized_url),
 
 827             escape(request.get_normalized_parameters()),
 
 830         key = '%s&' % escape(consumer.secret)
 
 832             key += escape(token.secret)
 
 836     def sign(self, request, consumer, token):
 
 837         """Builds the base signature string."""
 
 838         key, raw = self.signing_base(request, consumer, token)
 
 840         hashed = hmac.new(key, raw, sha)
 
 842         # Calculate the digest base 64.
 
 843         return binascii.b2a_base64(hashed.digest())[:-1]
 
 846 class SignatureMethod_PLAINTEXT(SignatureMethod):
 
 850     def signing_base(self, request, consumer, token):
 
 851         """Concatenates the consumer key and secret with the token's
 
 853         sig = '%s&' % escape(consumer.secret)
 
 855             sig = sig + escape(token.secret)
 
 858     def sign(self, request, consumer, token):
 
 859         key, raw = self.signing_base(request, consumer, token)