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)