Home | History | Annotate | Download | only in oauth2
      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).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.startswith('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 following: %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
    759