├── tests ├── __init__.py └── test_oauth.py ├── oauth2 ├── clients │ ├── __init__.py │ ├── imap.py │ └── smtp.py ├── _version.py ├── _compat.py └── __init__.py ├── requirements.txt ├── .gitignore ├── setup.cfg ├── .travis.yml ├── tox.ini ├── LICENSE.txt ├── setup.py ├── README.md ├── example ├── appengine_oauth.py ├── client.py └── server.py └── Makefile /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /oauth2/clients/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | httplib2==0.9.1 2 | mock==1.3.0 3 | pep8==1.6.2 4 | pytest==2.7.2 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py? 2 | *.egg-info 3 | *.swp 4 | .coverage 5 | coverage.xml 6 | nosetests.xml 7 | .tox 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | nocapture=1 3 | cover-package=oauth2 4 | cover-erase=1 5 | 6 | [bdist_wheel] 7 | universal=1 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | - "3.7-dev" 9 | - "nightly" 10 | install: 11 | - pip install -r requirements.txt 12 | - pip install codecov pytest-cov 13 | script: 14 | - py.test --cov=oauth2 15 | after_success: 16 | - codecov 17 | -------------------------------------------------------------------------------- /oauth2/_version.py: -------------------------------------------------------------------------------- 1 | # This is the version of this source code. 2 | 3 | manual_verstr = "1.9" 4 | 5 | 6 | 7 | auto_build_num = "0.post1" 8 | 9 | 10 | 11 | verstr = manual_verstr + "." + auto_build_num 12 | try: 13 | from pyutil.version_class import Version as pyutil_Version 14 | except (ImportError, ValueError): #pragma NO COVER 15 | # Maybe there is no pyutil installed. 16 | from distutils.version import LooseVersion as distutils_Version 17 | __version__ = distutils_Version(verstr) 18 | else: #pragma NO COVER 19 | __version__ = pyutil_Version(verstr) 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py26,py27,py32,py33,cover 4 | 5 | [testenv] 6 | commands = 7 | python setup.py test -q 8 | deps = 9 | httplib2 10 | coverage 11 | mock 12 | 13 | [testenv:cover] 14 | basepython = 15 | python2.7 16 | commands = 17 | nosetests --with-xunit --with-xcoverage 18 | deps = 19 | httplib2 20 | coverage 21 | mock 22 | nose 23 | nosexcover 24 | 25 | # we separate coverage into its own testenv because a) "last run wins" wrt 26 | # cobertura jenkins reporting and b) pypy and jython can't handle any 27 | # combination of versions of coverage and nosexcover that i can find. 28 | 29 | [testenv:docs] 30 | basepython = 31 | python2.6 32 | commands = 33 | sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html 34 | sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest 35 | deps = 36 | Sphinx 37 | repoze.sphinx.autointerface 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2007 Leah Culver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /oauth2/_compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | TEXT = unicode 3 | except NameError: #pragma NO COVER Py3k 4 | PY3 = True 5 | TEXT = str 6 | STRING_TYPES = (str, bytes) 7 | def b(x, encoding='ascii'): 8 | return bytes(x, encoding) 9 | else: #pragma NO COVER Python2 10 | PY3 = False 11 | STRING_TYPES = (unicode, bytes) 12 | def b(x, encoding='ascii'): 13 | if isinstance(x, unicode): 14 | x = x.encode(encoding) 15 | return x 16 | 17 | def u(x, encoding='ascii'): 18 | if isinstance(x, TEXT): #pragma NO COVER 19 | return x 20 | try: 21 | return x.decode(encoding) 22 | except AttributeError: #pragma NO COVER 23 | raise ValueError('WTF: %s' % x) 24 | 25 | try: 26 | import urlparse 27 | except ImportError: #pragma NO COVER Py3k 28 | from urllib.parse import parse_qs 29 | from urllib.parse import parse_qsl 30 | from urllib.parse import quote 31 | from urllib.parse import unquote 32 | from urllib.parse import unquote_to_bytes 33 | from urllib.parse import urlencode 34 | from urllib.parse import urlsplit 35 | from urllib.parse import urlunsplit 36 | from urllib.parse import urlparse 37 | from urllib.parse import urlunparse 38 | else: #pragma NO COVER Python2 39 | from urlparse import parse_qs 40 | from urlparse import parse_qsl 41 | from urllib import quote 42 | from urllib import unquote 43 | from urllib import urlencode 44 | from urlparse import urlsplit 45 | from urlparse import urlunsplit 46 | from urlparse import urlparse 47 | from urlparse import urlunparse 48 | unquote_to_bytes = unquote 49 | -------------------------------------------------------------------------------- /oauth2/clients/imap.py: -------------------------------------------------------------------------------- 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 oauth2 26 | import imaplib 27 | 28 | 29 | class IMAP4_SSL(imaplib.IMAP4_SSL): 30 | """IMAP wrapper for imaplib.IMAP4_SSL that implements XOAUTH.""" 31 | 32 | def authenticate(self, url, consumer, token): 33 | if consumer is not None and not isinstance(consumer, oauth2.Consumer): 34 | raise ValueError("Invalid consumer.") 35 | 36 | if token is not None and not isinstance(token, oauth2.Token): 37 | raise ValueError("Invalid token.") 38 | 39 | imaplib.IMAP4_SSL.authenticate(self, 'XOAUTH', 40 | lambda x: oauth2.build_xoauth_string(url, consumer, token)) 41 | -------------------------------------------------------------------------------- /oauth2/clients/smtp.py: -------------------------------------------------------------------------------- 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 oauth2 26 | import smtplib 27 | import base64 28 | 29 | 30 | class SMTP(smtplib.SMTP): 31 | """SMTP wrapper for smtplib.SMTP that implements XOAUTH.""" 32 | 33 | def authenticate(self, url, consumer, token): 34 | if consumer is not None and not isinstance(consumer, oauth2.Consumer): 35 | raise ValueError("Invalid consumer.") 36 | 37 | if token is not None and not isinstance(token, oauth2.Token): 38 | raise ValueError("Invalid token.") 39 | 40 | self.docmd('AUTH', 'XOAUTH %s' % \ 41 | base64.b64encode(oauth2.build_xoauth_string(url, consumer, token))) 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | from setuptools import setup, find_packages 4 | import os, re 5 | 6 | PKG='oauth2' 7 | VERSIONFILE = os.path.join('oauth2', '_version.py') 8 | verstr = "unknown" 9 | try: 10 | verstrline = open(VERSIONFILE, "rt").read() 11 | except EnvironmentError: 12 | pass # Okay, there is no version file. 13 | else: 14 | MVSRE = r"^manual_verstr *= *['\"]([^'\"]*)['\"]" 15 | mo = re.search(MVSRE, verstrline, re.M) 16 | if mo: 17 | mverstr = mo.group(1) 18 | else: 19 | print("unable to find version in %s" % (VERSIONFILE)) 20 | raise RuntimeError("if %s.py exists, it must be well-formed" % (VERSIONFILE,)) 21 | AVSRE = r"^auto_build_num *= *['\"]([^'\"]*)['\"]" 22 | mo = re.search(AVSRE, verstrline, re.M) 23 | if mo: 24 | averstr = mo.group(1) 25 | else: 26 | averstr = '' 27 | verstr = '.'.join([mverstr, averstr]) 28 | 29 | setup(name=PKG, 30 | version=verstr, 31 | description="library for OAuth version 1.9", 32 | author="Joe Stump", 33 | author_email="joe@simplegeo.com", 34 | url="http://github.com/joestump/python-oauth2", 35 | classifiers=[ 36 | "Intended Audience :: Developers", 37 | "Programming Language :: Python :: 2", 38 | "Programming Language :: Python :: 2.6", 39 | "Programming Language :: Python :: 2.7", 40 | "Programming Language :: Python :: 3", 41 | "Programming Language :: Python :: 3.3", 42 | "Programming Language :: Python :: 3.4", 43 | "Programming Language :: Python :: Implementation :: CPython", 44 | "Development Status :: 5 - Production/Stable", 45 | "Natural Language :: English", 46 | "License :: OSI Approved :: MIT License" 47 | ], 48 | packages = find_packages(exclude=['tests']), 49 | install_requires = ['httplib2'], 50 | license = "MIT License", 51 | keywords="oauth", 52 | zip_safe = True, 53 | test_suite="tests", 54 | tests_require=['mock']) 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Join the chat at https://gitter.im/joestump/python-oauth2](https://img.shields.io/badge/gitter-join%20chat-1dce73.svg?style=flat-square)](https://gitter.im/joestump/python-oauth2?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](http://img.shields.io/travis-ci/joestump/python-oauth2.png?branch=master&style=flat-square)](https://travis-ci.org/joestump/python-oauth2) [![Coverage](https://img.shields.io/codecov/c/github/joestump/python-oauth2.svg?style=flat-square)](https://codecov.io/gh/joestump/python-oauth2) ![Number of issues](https://img.shields.io/github/issues/joestump/python-oauth2.svg?style=flat-square) ![Licence MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square) 2 | 3 | ## Note: This library implements OAuth 1.0 and *not OAuth 2.0*. 4 | 5 | # Overview 6 | python-oauth2 is a python oauth library fully compatible with python versions: 2.6, 2.7, 3.3 and 3.4. This library is depended on by many other downstream packages such as Flask-Oauth. 7 | 8 | # Installing 9 | 10 | You can install `oauth2` via [the PIP package](https://pypi.python.org/pypi/oauth2). 11 | 12 | $ pip install oauth2 13 | 14 | We recommend using [virtualenv](https://virtualenv.pypa.io/en/latest/). 15 | 16 | # Examples 17 | 18 | Examples can be found in the [wiki](https://github.com/joestump/python-oauth2/wiki) 19 | 20 | # Running tests 21 | You can run tests using the following at the command line: 22 | 23 | $ pip install -r requirements.txt 24 | $ python setup.py test 25 | 26 | 27 | # History 28 | 29 | This code was originally forked from [Leah Culver and Andy Smith's oauth.py code](http://github.com/leah/python-oauth/). Some of the tests come from a [fork by Vic Fryzel](http://github.com/shellsage/python-oauth), while a revamped Request class and more tests were merged in from [Mark Paschal's fork](http://github.com/markpasc/python-oauth). A number of notable differences exist between this code and its forefathers: 30 | 31 | * 100% unit test coverage. 32 | * The DataStore object has been completely ripped out. While creating unit tests for the library I found several substantial bugs with the implementation and confirmed with Andy Smith that it was never fully baked. 33 | * Classes are no longer prefixed with OAuth. 34 | * The Request class now extends from dict. 35 | * The library is likely no longer compatible with Python 2.3. 36 | * The Client class works and extends from httplib2. It's a thin wrapper that handles automatically signing any normal HTTP request you might wish to make. 37 | -------------------------------------------------------------------------------- /example/appengine_oauth.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License 3 | 4 | Copyright (c) 2010 Justin Plock 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 os 26 | 27 | from google.appengine.ext import webapp 28 | from google.appengine.ext import db 29 | from google.appengine.ext.webapp import util 30 | import oauth2 as oauth # httplib2 is required for this to work on AppEngine 31 | 32 | class Client(db.Model): 33 | # oauth_key is the Model's key_name field 34 | oauth_secret = db.StringProperty() # str(uuid.uuid4()) works well for this 35 | first_name = db.StringProperty() 36 | last_name = db.StringProperty() 37 | email_address = db.EmailProperty(required=True) 38 | password = db.StringProperty(required=True) 39 | 40 | @property 41 | def secret(self): 42 | return self.oauth_secret 43 | 44 | class OAuthHandler(webapp.RequestHandler): 45 | 46 | def __init__(self): 47 | self._server = oauth.Server() 48 | self._server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1()) 49 | self._server.add_signature_method(oauth.SignatureMethod_PLAINTEXT()) 50 | 51 | def get_oauth_request(self): 52 | """Return an OAuth Request object for the current request.""" 53 | 54 | try: 55 | method = os.environ['REQUEST_METHOD'] 56 | except: 57 | method = 'GET' 58 | 59 | postdata = None 60 | if method in ('POST', 'PUT'): 61 | postdata = self.request.body 62 | 63 | return oauth.Request.from_request(method, self.request.uri, 64 | headers=self.request.headers, query_string=postdata) 65 | 66 | def get_client(self, request=None): 67 | """Return the client from the OAuth parameters.""" 68 | 69 | if not isinstance(request, oauth.Request): 70 | request = self.get_oauth_request() 71 | client_key = request.get_parameter('oauth_consumer_key') 72 | if not client_key: 73 | raise Exception('Missing "oauth_consumer_key" parameter in ' \ 74 | 'OAuth "Authorization" header') 75 | 76 | client = models.Client.get_by_key_name(client_key) 77 | if not client: 78 | raise Exception('Client "%s" not found.' % client_key) 79 | 80 | return client 81 | 82 | def is_valid(self): 83 | """Returns a Client object if this is a valid OAuth request.""" 84 | 85 | try: 86 | request = self.get_oauth_request() 87 | client = self.get_client(request) 88 | params = self._server.verify_request(request, client, None) 89 | except Exception as e: 90 | raise e 91 | 92 | return client 93 | 94 | class SampleHandler(OAuthHandler): 95 | def get(self): 96 | try: 97 | client = self.is_valid() 98 | except Exception as e: 99 | self.error(500) 100 | self.response.out.write(e) 101 | 102 | def main(): 103 | application = webapp.WSGIApplication([(r'/sample', SampleHandler)], 104 | debug=False) 105 | util.run_wsgi_app(application) 106 | 107 | if __name__ == '__main__': 108 | main() 109 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON = $(shell test -x bin/python && echo bin/python || \ 2 | echo `which python`) 3 | PYVERS = $(shell $(PYTHON) -c 'import sys; print "%s.%s" % sys.version_info[0:2]') 4 | VIRTUALENV = $(shell /bin/echo -n `which virtualenv || \ 5 | which virtualenv-$(PYVERS) || \ 6 | which virtualenv$(PYVERS)`) 7 | VIRTUALENV += --no-site-packages 8 | PAGER ?= less 9 | DEPS := $(shell find $(PWD)/deps -type f -printf "file://%p ") 10 | COVERAGE = $(shell test -x bin/coverage && echo bin/coverage || echo true) 11 | SETUP = $(PYTHON) ./setup.py 12 | EZ_INSTALL = $(SETUP) easy_install -f "$(DEPS)" 13 | PYLINT = bin/pylint 14 | PLATFORM = $(shell $(PYTHON) -c "from pkg_resources import get_build_platform; print get_build_platform()") 15 | OS := $(shell uname) 16 | EGG := $(shell $(SETUP) --fullname)-py$(PYVERS).egg 17 | SDIST := $(shell $(SETUP) --fullname).tar.gs 18 | SRCDIR := oauth2 19 | SOURCES := $(shell find $(SRCDIR) -type f -name \*.py -not -name 'test_*') 20 | TESTS := $(shell find $(SRCDIR) -type f -name test_\*.py) 21 | COVERED := $(SOURCES) 22 | ROOT = $(shell pwd) 23 | ROOTCMD = fakeroot 24 | SIGN_KEY ?= nerds@simplegeo.com 25 | BUILD_NUMBER ?= 1 26 | 27 | 28 | .PHONY: test dev clean extraclean debian/changelog 29 | 30 | all: egg 31 | egg: dist/$(EGG) 32 | 33 | dist/$(EGG): 34 | $(SETUP) bdist_egg 35 | 36 | sdist: 37 | $(SETUP) sdist 38 | 39 | debian/changelog: 40 | -git branch -D changelog 41 | git checkout -b changelog 42 | git-dch -a -N $(shell $(SETUP) --version) --debian-branch changelog \ 43 | --snapshot --snapshot-number=$(BUILD_NUMBER) 44 | 45 | deb: debian/changelog 46 | test -d dist/deb || mkdir -p dist/deb 47 | dpkg-buildpackage -r$(ROOTCMD) -k$(SIGN_KEY) 48 | mv ../python-oauth2_* dist/deb 49 | 50 | test: 51 | $(SETUP) test --with-coverage --cover-package=oauth2 52 | 53 | sdist: 54 | python setup.py sdist 55 | 56 | xunit.xml: bin/nosetests $(SOURCES) $(TESTS) 57 | $(SETUP) test --with-xunit --xunit-file=$@ 58 | 59 | bin/nosetests: bin/easy_install 60 | @$(EZ_INSTALL) nose 61 | 62 | coverage: .coverage 63 | @$(COVERAGE) html -d $@ $(COVERED) 64 | 65 | coverage.xml: .coverage 66 | @$(COVERAGE) xml $(COVERED) 67 | 68 | .coverage: $(SOURCES) $(TESTS) bin/coverage bin/nosetests 69 | -@$(COVERAGE) run $(SETUP) test 70 | 71 | bin/coverage: bin/easy_install 72 | @$(EZ_INSTALL) coverage 73 | 74 | profile: .profile bin/pyprof2html 75 | bin/pyprof2html -o $@ $< 76 | 77 | .profile: $(SOURCES) bin/nosetests 78 | -$(SETUP) test -q --with-profile --profile-stats-file=$@ 79 | 80 | bin/pyprof2html: bin/easy_install bin/ 81 | @$(EZ_INSTALL) pyprof2html 82 | 83 | docs: $(SOURCES) bin/epydoc 84 | @echo bin/epydoc -q --html --no-frames -o $@ ... 85 | @bin/epydoc -q --html --no-frames -o $@ $(SOURCES) 86 | 87 | bin/epydoc: bin/easy_install 88 | @$(EZ_INSTALL) epydoc 89 | 90 | bin/pep8: bin/easy_install 91 | @$(EZ_INSTALL) pep8 92 | 93 | pep8: bin/pep8 94 | @bin/pep8 --repeat --ignore E225 $(SRCDIR) 95 | 96 | pep8.txt: bin/pep8 97 | @bin/pep8 --repeat --ignore E225 $(SRCDIR) > $@ 98 | 99 | lint: bin/pylint 100 | -$(PYLINT) -f colorized $(SRCDIR) 101 | 102 | lint.html: bin/pylint 103 | -$(PYLINT) -f html $(SRCDIR) > $@ 104 | 105 | lint.txt: bin/pylint 106 | -$(PYLINT) -f parseable $(SRCDIR) > $@ 107 | 108 | bin/pylint: bin/easy_install 109 | @$(EZ_INSTALL) pylint 110 | 111 | README.html: README.mkd | bin/markdown 112 | bin/markdown -e utf-8 $^ -f $@ 113 | 114 | bin/markdown: bin/easy_install 115 | @$(EZ_INSTALL) Markdown 116 | 117 | 118 | # Development setup 119 | rtfm: 120 | $(PAGER) README.mkd 121 | 122 | tags: TAGS.gz 123 | 124 | TAGS.gz: TAGS 125 | gzip $^ 126 | 127 | TAGS: $(SOURCES) 128 | ctags -eR . 129 | 130 | env: bin/easy_install 131 | 132 | bin/easy_install: 133 | $(VIRTUALENV) . 134 | -test -f deps/setuptools* && $@ -U deps/setuptools* 135 | 136 | dev: develop 137 | develop: env 138 | nice -n 20 $(SETUP) develop 139 | @echo " ---------------------------------------------" 140 | @echo " To activate the development environment, run:" 141 | @echo " . bin/activate" 142 | @echo " ---------------------------------------------" 143 | 144 | clean: 145 | clean: 146 | find . -type f -name \*.pyc -exec rm {} \; 147 | rm -rf build dist TAGS TAGS.gz digg.egg-info tmp .coverage \ 148 | coverage coverage.xml docs lint.html lint.txt profile \ 149 | .profile *.egg xunit.xml 150 | @if test "$(OS)" = "Linux"; then $(ROOTCMD) debian/rules clean; fi 151 | 152 | 153 | xclean: extraclean 154 | extraclean: clean 155 | rm -rf bin lib .Python include 156 | -------------------------------------------------------------------------------- /example/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License 3 | 4 | Copyright (c) 2007 Leah Culver 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 | Example consumer. This is not recommended for production. 25 | Instead, you'll want to create your own subclass of OAuthClient 26 | or find one that works with your web framework. 27 | """ 28 | 29 | import httplib 30 | import time 31 | import oauth.oauth as oauth 32 | 33 | # settings for the local test consumer 34 | SERVER = 'localhost' 35 | PORT = 8080 36 | 37 | # fake urls for the test server (matches ones in server.py) 38 | REQUEST_TOKEN_URL = 'https://photos.example.net/request_token' 39 | ACCESS_TOKEN_URL = 'https://photos.example.net/access_token' 40 | AUTHORIZATION_URL = 'https://photos.example.net/authorize' 41 | CALLBACK_URL = 'http://printer.example.com/request_token_ready' 42 | RESOURCE_URL = 'http://photos.example.net/photos' 43 | 44 | # key and secret granted by the service provider for this consumer 45 | # application - same as the MockOAuthDataStore 46 | CONSUMER_KEY = 'key' 47 | CONSUMER_SECRET = 'secret' 48 | 49 | # example client using httplib with headers 50 | class SimpleOAuthClient(oauth.OAuthClient): 51 | 52 | def __init__(self, server, port=httplib.HTTP_PORT, request_token_url='', 53 | access_token_url='', authorization_url=''): 54 | self.server = server 55 | self.port = port 56 | self.request_token_url = request_token_url 57 | self.access_token_url = access_token_url 58 | self.authorization_url = authorization_url 59 | self.connection = httplib.HTTPConnection( 60 | "%s:%d" % (self.server, self.port)) 61 | 62 | def fetch_request_token(self, oauth_request): 63 | # via headers 64 | # -> OAuthToken 65 | self.connection.request(oauth_request.http_method, 66 | self.request_token_url, headers=oauth_request.to_header()) 67 | response = self.connection.getresponse() 68 | return oauth.OAuthToken.from_string(response.read()) 69 | 70 | def fetch_access_token(self, oauth_request): 71 | # via headers 72 | # -> OAuthToken 73 | self.connection.request(oauth_request.http_method, 74 | self.access_token_url, headers=oauth_request.to_header()) 75 | response = self.connection.getresponse() 76 | return oauth.OAuthToken.from_string(response.read()) 77 | 78 | def authorize_token(self, oauth_request): 79 | # via url 80 | # -> typically just some okay response 81 | self.connection.request(oauth_request.http_method, 82 | oauth_request.to_url()) 83 | response = self.connection.getresponse() 84 | return response.read() 85 | 86 | def access_resource(self, oauth_request): 87 | # via post body 88 | # -> some protected resources 89 | headers = {'Content-Type' :'application/x-www-form-urlencoded'} 90 | self.connection.request('POST', RESOURCE_URL, 91 | body=oauth_request.to_postdata(), 92 | headers=headers) 93 | response = self.connection.getresponse() 94 | return response.read() 95 | 96 | def run_example(): 97 | 98 | # setup 99 | print('** OAuth Python Library Example **') 100 | client = SimpleOAuthClient(SERVER, PORT, REQUEST_TOKEN_URL, 101 | ACCESS_TOKEN_URL, AUTHORIZATION_URL) 102 | consumer = oauth.OAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET) 103 | signature_method_plaintext = oauth.OAuthSignatureMethod_PLAINTEXT() 104 | signature_method_hmac_sha1 = oauth.OAuthSignatureMethod_HMAC_SHA1() 105 | pause() 106 | 107 | # get request token 108 | print('* Obtain a request token ...') 109 | pause() 110 | oauth_request = oauth.OAuthRequest.from_consumer_and_token( 111 | consumer, callback=CALLBACK_URL, http_url=client.request_token_url) 112 | oauth_request.sign_request(signature_method_plaintext, consumer, None) 113 | print('REQUEST (via headers)') 114 | print('parameters: %s' % str(oauth_request.parameters)) 115 | pause() 116 | token = client.fetch_request_token(oauth_request) 117 | print('GOT') 118 | print('key: %s' % str(token.key)) 119 | print('secret: %s' % str(token.secret)) 120 | print('callback confirmed? %s' % str(token.callback_confirmed)) 121 | pause() 122 | 123 | print('* Authorize the request token ...') 124 | pause() 125 | oauth_request = oauth.OAuthRequest.from_token_and_callback( 126 | token=token, http_url=client.authorization_url) 127 | print('REQUEST (via url query string)') 128 | print('parameters: %s' % str(oauth_request.parameters)) 129 | pause() 130 | # this will actually occur only on some callback 131 | response = client.authorize_token(oauth_request) 132 | print('GOT') 133 | print(response) 134 | # sad way to get the verifier 135 | import urlparse, cgi 136 | query = urlparse.urlparse(response)[4] 137 | params = cgi.parse_qs(query, keep_blank_values=False) 138 | verifier = params['oauth_verifier'][0] 139 | print('verifier: %s' % verifier) 140 | pause() 141 | 142 | # get access token 143 | print('* Obtain an access token ...') 144 | pause() 145 | oauth_request = oauth.OAuthRequest.from_consumer_and_token( 146 | consumer, token=token, verifier=verifier, 147 | http_url=client.access_token_url) 148 | oauth_request.sign_request(signature_method_plaintext, consumer, token) 149 | print('REQUEST (via headers)') 150 | print('parameters: %s' % str(oauth_request.parameters)) 151 | pause() 152 | token = client.fetch_access_token(oauth_request) 153 | print('GOT') 154 | print('key: %s' % str(token.key)) 155 | print('secret: %s' % str(token.secret)) 156 | pause() 157 | 158 | # access some protected resources 159 | print('* Access protected resources ...') 160 | pause() 161 | parameters = {'file': 'vacation.jpg', 162 | 'size': 'original'} # resource specific params 163 | oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, 164 | token=token, http_method='POST', http_url=RESOURCE_URL, 165 | parameters=parameters) 166 | oauth_request.sign_request(signature_method_hmac_sha1, consumer, token) 167 | print('REQUEST (via post body)') 168 | print('parameters: %s' % str(oauth_request.parameters)) 169 | pause() 170 | params = client.access_resource(oauth_request) 171 | print('GOT') 172 | print('non-oauth parameters: %s' % params) 173 | pause() 174 | 175 | def pause(): 176 | print('') 177 | time.sleep(1) 178 | 179 | if __name__ == '__main__': 180 | run_example() 181 | print('Done.') 182 | -------------------------------------------------------------------------------- /example/server.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License 3 | 4 | Copyright (c) 2007 Leah Culver 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 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 26 | import urllib 27 | 28 | import oauth.oauth as oauth 29 | 30 | # fake urls for the test server 31 | REQUEST_TOKEN_URL = 'https://photos.example.net/request_token' 32 | ACCESS_TOKEN_URL = 'https://photos.example.net/access_token' 33 | AUTHORIZATION_URL = 'https://photos.example.net/authorize' 34 | CALLBACK_URL = 'http://printer.example.com/request_token_ready' 35 | RESOURCE_URL = 'http://photos.example.net/photos' 36 | REALM = 'http://photos.example.net/' 37 | VERIFIER = 'verifier' 38 | 39 | # example store for one of each thing 40 | class MockOAuthDataStore(oauth.OAuthDataStore): 41 | 42 | def __init__(self): 43 | self.consumer = oauth.OAuthConsumer('key', 'secret') 44 | self.request_token = oauth.OAuthToken('requestkey', 'requestsecret') 45 | self.access_token = oauth.OAuthToken('accesskey', 'accesssecret') 46 | self.nonce = 'nonce' 47 | self.verifier = VERIFIER 48 | 49 | def lookup_consumer(self, key): 50 | if key == self.consumer.key: 51 | return self.consumer 52 | return None 53 | 54 | def lookup_token(self, token_type, token): 55 | token_attrib = getattr(self, '%s_token' % token_type) 56 | if token == token_attrib.key: 57 | ## HACK 58 | token_attrib.set_callback(CALLBACK_URL) 59 | return token_attrib 60 | return None 61 | 62 | def lookup_nonce(self, oauth_consumer, oauth_token, nonce): 63 | if (oauth_token and 64 | oauth_consumer.key == self.consumer.key and 65 | (oauth_token.key == self.request_token.key or 66 | oauth_token.key == self.access_token.key) and 67 | nonce == self.nonce): 68 | return self.nonce 69 | return None 70 | 71 | def fetch_request_token(self, oauth_consumer, oauth_callback): 72 | if oauth_consumer.key == self.consumer.key: 73 | if oauth_callback: 74 | # want to check here if callback is sensible 75 | # for mock store, we assume it is 76 | self.request_token.set_callback(oauth_callback) 77 | return self.request_token 78 | return None 79 | 80 | def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier): 81 | if (oauth_consumer.key == self.consumer.key and 82 | oauth_token.key == self.request_token.key and 83 | oauth_verifier == self.verifier): 84 | # want to check here if token is authorized 85 | # for mock store, we assume it is 86 | return self.access_token 87 | return None 88 | 89 | def authorize_request_token(self, oauth_token, user): 90 | if oauth_token.key == self.request_token.key: 91 | # authorize the request token in the store 92 | # for mock store, do nothing 93 | return self.request_token 94 | return None 95 | 96 | class RequestHandler(BaseHTTPRequestHandler): 97 | 98 | def __init__(self, *args, **kwargs): 99 | self.oauth_server = oauth.OAuthServer(MockOAuthDataStore()) 100 | self.oauth_server.add_signature_method( 101 | oauth.OAuthSignatureMethod_PLAINTEXT()) 102 | self.oauth_server.add_signature_method( 103 | oauth.OAuthSignatureMethod_HMAC_SHA1()) 104 | BaseHTTPRequestHandler.__init__(self, *args, **kwargs) 105 | 106 | # example way to send an oauth error 107 | def send_oauth_error(self, err=None): 108 | # send a 401 error 109 | self.send_error(401, str(err.message)) 110 | # return the authenticate header 111 | header = oauth.build_authenticate_header(realm=REALM) 112 | for k, v in header.iteritems(): 113 | self.send_header(k, v) 114 | 115 | def do_GET(self): 116 | 117 | # debug info 118 | #print self.command, self.path, self.headers 119 | 120 | # get the post data (if any) 121 | postdata = None 122 | if self.command == 'POST': 123 | try: 124 | length = int(self.headers.getheader('content-length')) 125 | postdata = self.rfile.read(length) 126 | except: 127 | pass 128 | 129 | # construct the oauth request from the request parameters 130 | oauth_request = oauth.OAuthRequest.from_request(self.command, 131 | self.path, headers=self.headers, query_string=postdata) 132 | 133 | # request token 134 | if self.path.startswith(REQUEST_TOKEN_URL): 135 | try: 136 | # create a request token 137 | token = self.oauth_server.fetch_request_token(oauth_request) 138 | # send okay response 139 | self.send_response(200, 'OK') 140 | self.end_headers() 141 | # return the token 142 | self.wfile.write(token.to_string()) 143 | except oauth.OAuthError as err: 144 | self.send_oauth_error(err) 145 | return 146 | 147 | # user authorization 148 | if self.path.startswith(AUTHORIZATION_URL): 149 | try: 150 | # get the request token 151 | token = self.oauth_server.fetch_request_token(oauth_request) 152 | # authorize the token (kind of does nothing for now) 153 | token = self.oauth_server.authorize_token(token, None) 154 | token.set_verifier(VERIFIER) 155 | # send okay response 156 | self.send_response(200, 'OK') 157 | self.end_headers() 158 | # return the callback url (to show server has it) 159 | self.wfile.write(token.get_callback_url()) 160 | except oauth.OAuthError as err: 161 | self.send_oauth_error(err) 162 | return 163 | 164 | # access token 165 | if self.path.startswith(ACCESS_TOKEN_URL): 166 | try: 167 | # create an access token 168 | token = self.oauth_server.fetch_access_token(oauth_request) 169 | # send okay response 170 | self.send_response(200, 'OK') 171 | self.end_headers() 172 | # return the token 173 | self.wfile.write(token.to_string()) 174 | except oauth.OAuthError as err: 175 | self.send_oauth_error(err) 176 | return 177 | 178 | # protected resources 179 | if self.path.startswith(RESOURCE_URL): 180 | try: 181 | # verify the request has been oauth authorized 182 | consumer, token, params = self.oauth_server.verify_request( 183 | oauth_request) 184 | # send okay response 185 | self.send_response(200, 'OK') 186 | self.end_headers() 187 | # return the extra parameters - just for something to return 188 | self.wfile.write(str(params)) 189 | except oauth.OAuthError as err: 190 | self.send_oauth_error(err) 191 | return 192 | 193 | def do_POST(self): 194 | return self.do_GET() 195 | 196 | def main(): 197 | try: 198 | server = HTTPServer(('', 8080), RequestHandler) 199 | print('Test server running...') 200 | server.serve_forever() 201 | except KeyboardInterrupt: 202 | server.socket.close() 203 | 204 | if __name__ == '__main__': 205 | main() 206 | -------------------------------------------------------------------------------- /oauth2/__init__.py: -------------------------------------------------------------------------------- 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 base64 26 | from hashlib import sha1 27 | import time 28 | import random 29 | import hmac 30 | import binascii 31 | import httplib2 32 | 33 | from ._compat import PY3 34 | from ._compat import b 35 | from ._compat import parse_qs 36 | from ._compat import quote 37 | from ._compat import STRING_TYPES 38 | from ._compat import TEXT 39 | from ._compat import u 40 | from ._compat import unquote 41 | from ._compat import unquote_to_bytes 42 | from ._compat import urlencode 43 | from ._compat import urlsplit 44 | from ._compat import urlunsplit 45 | from ._compat import urlparse 46 | from ._compat import urlunparse 47 | from ._version import __version__ 48 | 49 | OAUTH_VERSION = '1.0' # Hi Blaine! 50 | HTTP_METHOD = 'GET' 51 | SIGNATURE_METHOD = 'PLAINTEXT' 52 | 53 | 54 | class Error(RuntimeError): 55 | """Generic exception class.""" 56 | 57 | def __init__(self, message='OAuth error occurred.'): 58 | self._message = message 59 | 60 | @property 61 | def message(self): 62 | """A hack to get around the deprecation errors in 2.6.""" 63 | return self._message 64 | 65 | def __str__(self): 66 | return self._message 67 | 68 | 69 | class MissingSignature(Error): 70 | pass 71 | 72 | 73 | def build_authenticate_header(realm=''): 74 | """Optional WWW-Authenticate header (401 error)""" 75 | return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} 76 | 77 | 78 | def build_xoauth_string(url, consumer, token=None): 79 | """Build an XOAUTH string for use in SMTP/IMPA authentication.""" 80 | request = Request.from_consumer_and_token(consumer, token, 81 | "GET", url) 82 | 83 | signing_method = SignatureMethod_HMAC_SHA1() 84 | request.sign_request(signing_method, consumer, token) 85 | 86 | params = [] 87 | for k, v in sorted(request.items()): 88 | if v is not None: 89 | params.append('%s="%s"' % (k, escape(v))) 90 | 91 | return "%s %s %s" % ("GET", url, ','.join(params)) 92 | 93 | 94 | def to_unicode(s): 95 | """ Convert to unicode, raise exception with instructive error 96 | message if s is not unicode, ascii, or utf-8. """ 97 | if not isinstance(s, TEXT): 98 | if not isinstance(s, bytes): 99 | raise TypeError('You are required to pass either unicode or ' 100 | 'bytes here, not: %r (%s)' % (type(s), s)) 101 | try: 102 | s = s.decode('utf-8') 103 | except UnicodeDecodeError as le: 104 | raise TypeError('You are required to pass either a unicode ' 105 | 'object or a utf-8-enccoded bytes string here. ' 106 | 'You passed a bytes object which contained ' 107 | 'non-utf-8: %r. The UnicodeDecodeError that ' 108 | 'resulted from attempting to interpret it as ' 109 | 'utf-8 was: %s' 110 | % (s, le,)) 111 | return s 112 | 113 | def to_utf8(s): 114 | return to_unicode(s).encode('utf-8') 115 | 116 | def to_unicode_if_string(s): 117 | if isinstance(s, STRING_TYPES): 118 | return to_unicode(s) 119 | else: 120 | return s 121 | 122 | def to_utf8_if_string(s): 123 | if isinstance(s, STRING_TYPES): 124 | return to_utf8(s) 125 | else: 126 | return s 127 | 128 | def to_unicode_optional_iterator(x): 129 | """ 130 | Raise TypeError if x is a str containing non-utf8 bytes or if x is 131 | an iterable which contains such a str. 132 | """ 133 | if isinstance(x, STRING_TYPES): 134 | return to_unicode(x) 135 | 136 | try: 137 | l = list(x) 138 | except TypeError as e: 139 | assert 'is not iterable' in str(e) 140 | return x 141 | else: 142 | return [ to_unicode(e) for e in l ] 143 | 144 | def to_utf8_optional_iterator(x): 145 | """ 146 | Raise TypeError if x is a str or if x is an iterable which 147 | contains a str. 148 | """ 149 | if isinstance(x, STRING_TYPES): 150 | return to_utf8(x) 151 | 152 | try: 153 | l = list(x) 154 | except TypeError as e: 155 | assert 'is not iterable' in str(e) 156 | return x 157 | else: 158 | return [ to_utf8_if_string(e) for e in l ] 159 | 160 | def escape(s): 161 | """Escape a URL including any /.""" 162 | if not isinstance(s, bytes): 163 | s = s.encode('utf-8') 164 | return quote(s, safe='~') 165 | 166 | def generate_timestamp(): 167 | """Get seconds since epoch (UTC).""" 168 | return int(time.time()) 169 | 170 | 171 | def generate_nonce(length=8): 172 | """Generate pseudorandom number.""" 173 | return ''.join([str(random.SystemRandom().randint(0, 9)) for i in range(length)]) 174 | 175 | 176 | def generate_verifier(length=8): 177 | """Generate pseudorandom number.""" 178 | return ''.join([str(random.SystemRandom().randint(0, 9)) for i in range(length)]) 179 | 180 | 181 | class Consumer(object): 182 | """A consumer of OAuth-protected services. 183 | 184 | The OAuth consumer is a "third-party" service that wants to access 185 | protected resources from an OAuth service provider on behalf of an end 186 | user. It's kind of the OAuth client. 187 | 188 | Usually a consumer must be registered with the service provider by the 189 | developer of the consumer software. As part of that process, the service 190 | provider gives the consumer a *key* and a *secret* with which the consumer 191 | software can identify itself to the service. The consumer will include its 192 | key in each request to identify itself, but will use its secret only when 193 | signing requests, to prove that the request is from that particular 194 | registered consumer. 195 | 196 | Once registered, the consumer can then use its consumer credentials to ask 197 | the service provider for a request token, kicking off the OAuth 198 | authorization process. 199 | """ 200 | 201 | key = None 202 | secret = None 203 | 204 | def __init__(self, key, secret): 205 | self.key = key 206 | self.secret = secret 207 | 208 | if self.key is None or self.secret is None: 209 | raise ValueError("Key and secret must be set.") 210 | 211 | def __str__(self): 212 | data = {'oauth_consumer_key': self.key, 213 | 'oauth_consumer_secret': self.secret} 214 | 215 | return urlencode(data) 216 | 217 | 218 | class Token(object): 219 | """An OAuth credential used to request authorization or a protected 220 | resource. 221 | 222 | Tokens in OAuth comprise a *key* and a *secret*. The key is included in 223 | requests to identify the token being used, but the secret is used only in 224 | the signature, to prove that the requester is who the server gave the 225 | token to. 226 | 227 | When first negotiating the authorization, the consumer asks for a *request 228 | token* that the live user authorizes with the service provider. The 229 | consumer then exchanges the request token for an *access token* that can 230 | be used to access protected resources. 231 | """ 232 | 233 | key = None 234 | secret = None 235 | callback = None 236 | callback_confirmed = None 237 | verifier = None 238 | 239 | def __init__(self, key, secret): 240 | self.key = key 241 | self.secret = secret 242 | 243 | if self.key is None or self.secret is None: 244 | raise ValueError("Key and secret must be set.") 245 | 246 | def set_callback(self, callback): 247 | self.callback = callback 248 | self.callback_confirmed = 'true' 249 | 250 | def set_verifier(self, verifier=None): 251 | if verifier is not None: 252 | self.verifier = verifier 253 | else: 254 | self.verifier = generate_verifier() 255 | 256 | def get_callback_url(self): 257 | if self.callback and self.verifier: 258 | # Append the oauth_verifier. 259 | parts = urlparse(self.callback) 260 | scheme, netloc, path, params, query, fragment = parts[:6] 261 | if query: 262 | query = '%s&oauth_verifier=%s' % (query, self.verifier) 263 | else: 264 | query = 'oauth_verifier=%s' % self.verifier 265 | return urlunparse((scheme, netloc, path, params, 266 | query, fragment)) 267 | return self.callback 268 | 269 | def to_string(self): 270 | """Returns this token as a plain string, suitable for storage. 271 | 272 | The resulting string includes the token's secret, so you should never 273 | send or store this string where a third party can read it. 274 | """ 275 | items = [ 276 | ('oauth_token', self.key), 277 | ('oauth_token_secret', self.secret), 278 | ] 279 | 280 | if self.callback_confirmed is not None: 281 | items.append(('oauth_callback_confirmed', self.callback_confirmed)) 282 | return urlencode(items) 283 | 284 | @staticmethod 285 | def from_string(s): 286 | """Deserializes a token from a string like one returned by 287 | `to_string()`.""" 288 | 289 | if not len(s): 290 | raise ValueError("Invalid parameter string.") 291 | 292 | params = parse_qs(u(s), keep_blank_values=False) 293 | if not len(params): 294 | raise ValueError("Invalid parameter string.") 295 | 296 | try: 297 | key = params['oauth_token'][0] 298 | except Exception: 299 | raise ValueError("'oauth_token' not found in OAuth request.") 300 | 301 | try: 302 | secret = params['oauth_token_secret'][0] 303 | except Exception: 304 | raise ValueError("'oauth_token_secret' not found in " 305 | "OAuth request.") 306 | 307 | token = Token(key, secret) 308 | try: 309 | token.callback_confirmed = params['oauth_callback_confirmed'][0] 310 | except KeyError: 311 | pass # 1.0, no callback confirmed. 312 | return token 313 | 314 | def __str__(self): 315 | return self.to_string() 316 | 317 | 318 | def setter(attr): 319 | name = attr.__name__ 320 | 321 | def getter(self): 322 | try: 323 | return self.__dict__[name] 324 | except KeyError: 325 | raise AttributeError(name) 326 | 327 | def deleter(self): 328 | del self.__dict__[name] 329 | 330 | return property(getter, attr, deleter) 331 | 332 | 333 | class Request(dict): 334 | 335 | """The parameters and information for an HTTP request, suitable for 336 | authorizing with OAuth credentials. 337 | 338 | When a consumer wants to access a service's protected resources, it does 339 | so using a signed HTTP request identifying itself (the consumer) with its 340 | key, and providing an access token authorized by the end user to access 341 | those resources. 342 | 343 | """ 344 | 345 | version = OAUTH_VERSION 346 | 347 | def __init__(self, method=HTTP_METHOD, url=None, parameters=None, 348 | body=b'', is_form_encoded=False): 349 | if url is not None: 350 | self.url = to_unicode(url) 351 | self.method = method 352 | if parameters is not None: 353 | for k, v in parameters.items(): 354 | k = to_unicode(k) 355 | v = to_unicode_optional_iterator(v) 356 | 357 | self[k] = v 358 | self.body = body 359 | self.is_form_encoded = is_form_encoded 360 | 361 | @setter 362 | def url(self, value): 363 | self.__dict__['url'] = value 364 | if value is not None: 365 | scheme, netloc, path, query, fragment = urlsplit(value) 366 | 367 | # Exclude default port numbers. 368 | if scheme == 'http' and netloc[-3:] == ':80': 369 | netloc = netloc[:-3] 370 | elif scheme == 'https' and netloc[-4:] == ':443': 371 | netloc = netloc[:-4] 372 | if scheme not in ('http', 'https'): 373 | raise ValueError("Unsupported URL %s (%s)." % (value, scheme)) 374 | 375 | # Normalized URL excludes params, query, and fragment. 376 | self.normalized_url = urlunsplit((scheme, netloc, path, None, None)) 377 | else: 378 | self.normalized_url = None 379 | self.__dict__['url'] = None 380 | 381 | @setter 382 | def method(self, value): 383 | self.__dict__['method'] = value.upper() 384 | 385 | def _get_timestamp_nonce(self): 386 | return self['oauth_timestamp'], self['oauth_nonce'] 387 | 388 | def get_nonoauth_parameters(self): 389 | """Get any non-OAuth parameters.""" 390 | return dict([(k, v) for k, v in self.items() 391 | if not k.startswith('oauth_')]) 392 | 393 | def to_header(self, realm=''): 394 | """Serialize as a header for an HTTPAuth request.""" 395 | oauth_params = ((k, v) for k, v in self.items() 396 | if k.startswith('oauth_')) 397 | stringy_params = ((k, escape(v)) for k, v in oauth_params) 398 | header_params = ('%s="%s"' % (k, v) for k, v in stringy_params) 399 | params_header = ', '.join(header_params) 400 | 401 | auth_header = 'OAuth realm="%s"' % realm 402 | if params_header: 403 | auth_header = "%s, %s" % (auth_header, params_header) 404 | 405 | return {'Authorization': auth_header} 406 | 407 | def to_postdata(self): 408 | """Serialize as post data for a POST request.""" 409 | items = [] 410 | for k, v in sorted(self.items()): # predictable for testing 411 | items.append((k.encode('utf-8'), to_utf8_optional_iterator(v))) 412 | 413 | # tell urlencode to deal with sequence values and map them correctly 414 | # to resulting querystring. for example self["k"] = ["v1", "v2"] will 415 | # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D 416 | return urlencode(items, True).replace('+', '%20') 417 | 418 | def to_url(self): 419 | """Serialize as a URL for a GET request.""" 420 | base_url = urlparse(self.url) 421 | 422 | if PY3: 423 | query = parse_qs(base_url.query) 424 | for k, v in self.items(): 425 | query.setdefault(k, []).append(to_utf8_optional_iterator(v)) 426 | scheme = base_url.scheme 427 | netloc = base_url.netloc 428 | path = base_url.path 429 | params = base_url.params 430 | fragment = base_url.fragment 431 | else: 432 | query = parse_qs(to_utf8(base_url.query)) 433 | for k, v in self.items(): 434 | query.setdefault(to_utf8(k), []).append(to_utf8_optional_iterator(v)) 435 | scheme = to_utf8(base_url.scheme) 436 | netloc = to_utf8(base_url.netloc) 437 | path = to_utf8(base_url.path) 438 | params = to_utf8(base_url.params) 439 | fragment = to_utf8(base_url.fragment) 440 | 441 | url = (scheme, netloc, path, params, urlencode(query, True), fragment) 442 | return urlunparse(url) 443 | 444 | def get_parameter(self, parameter): 445 | ret = self.get(parameter) 446 | if ret is None: 447 | raise Error('Parameter not found: %s' % parameter) 448 | 449 | return ret 450 | 451 | def get_normalized_parameters(self): 452 | """Return a string that contains the parameters that must be signed.""" 453 | items = [] 454 | for key, value in self.items(): 455 | if key == 'oauth_signature': 456 | continue 457 | # 1.0a/9.1.1 states that kvp must be sorted by key, then by value, 458 | # so we unpack sequence values into multiple items for sorting. 459 | if isinstance(value, STRING_TYPES): 460 | items.append((to_utf8_if_string(key), to_utf8(value))) 461 | else: 462 | try: 463 | value = list(value) 464 | except TypeError as e: 465 | assert 'is not iterable' in str(e) 466 | items.append((to_utf8_if_string(key), to_utf8_if_string(value))) 467 | else: 468 | items.extend((to_utf8_if_string(key), to_utf8_if_string(item)) for item in value) 469 | 470 | # Include any query string parameters from the provided URL 471 | query = urlparse(self.url)[4] 472 | 473 | url_items = self._split_url_string(query).items() 474 | url_items = [(to_utf8(k), to_utf8_optional_iterator(v)) for k, v in url_items if k != 'oauth_signature' ] 475 | items.extend(url_items) 476 | 477 | items.sort() 478 | encoded_str = urlencode(items, True) 479 | # Encode signature parameters per Oauth Core 1.0 protocol 480 | # spec draft 7, section 3.6 481 | # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6) 482 | # Spaces must be encoded with "%20" instead of "+" 483 | return encoded_str.replace('+', '%20').replace('%7E', '~') 484 | 485 | def sign_request(self, signature_method, consumer, token): 486 | """Set the signature parameter to the result of sign.""" 487 | 488 | if not self.is_form_encoded: 489 | # according to 490 | # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html 491 | # section 4.1.1 "OAuth Consumers MUST NOT include an 492 | # oauth_body_hash parameter on requests with form-encoded 493 | # request bodies." 494 | if not self.body: 495 | self.body = '' 496 | self['oauth_body_hash'] = base64.b64encode(sha1(to_utf8(self.body)).digest()) 497 | 498 | if 'oauth_consumer_key' not in self: 499 | self['oauth_consumer_key'] = consumer.key 500 | 501 | if token and 'oauth_token' not in self: 502 | self['oauth_token'] = token.key 503 | 504 | self['oauth_signature_method'] = signature_method.name 505 | self['oauth_signature'] = signature_method.sign(self, consumer, token) 506 | 507 | @classmethod 508 | def make_timestamp(cls): 509 | """Get seconds since epoch (UTC).""" 510 | return str(int(time.time())) 511 | 512 | @classmethod 513 | def make_nonce(cls): 514 | """Generate pseudorandom number.""" 515 | return str(random.SystemRandom().randint(0, 100000000)) 516 | 517 | @classmethod 518 | def from_request(cls, http_method, http_url, headers=None, parameters=None, 519 | query_string=None): 520 | """Combines multiple parameter sources.""" 521 | if parameters is None: 522 | parameters = {} 523 | 524 | # Headers 525 | if headers: 526 | auth_header = None 527 | for k, v in headers.items(): 528 | if k.lower() == 'authorization' or \ 529 | k.upper() == 'HTTP_AUTHORIZATION': 530 | auth_header = v 531 | 532 | # Check that the authorization header is OAuth. 533 | if auth_header and auth_header[:6] == 'OAuth ': 534 | auth_header = auth_header[6:] 535 | try: 536 | # Get the parameters from the header. 537 | header_params = cls._split_header(auth_header) 538 | parameters.update(header_params) 539 | except: 540 | raise Error('Unable to parse OAuth parameters from ' 541 | 'Authorization header.') 542 | 543 | # GET or POST query string. 544 | if query_string: 545 | query_params = cls._split_url_string(query_string) 546 | 547 | parameters.update(query_params) 548 | 549 | # URL parameters. 550 | param_str = urlparse(http_url)[4] # query 551 | url_params = cls._split_url_string(param_str) 552 | parameters.update(url_params) 553 | 554 | if parameters: 555 | return cls(http_method, http_url, parameters) 556 | 557 | return None 558 | 559 | @classmethod 560 | def from_consumer_and_token(cls, consumer, token=None, 561 | http_method=HTTP_METHOD, http_url=None, parameters=None, 562 | body=b'', is_form_encoded=False): 563 | if not parameters: 564 | parameters = {} 565 | 566 | defaults = { 567 | 'oauth_consumer_key': consumer.key, 568 | 'oauth_timestamp': cls.make_timestamp(), 569 | 'oauth_nonce': cls.make_nonce(), 570 | 'oauth_version': cls.version, 571 | } 572 | 573 | defaults.update(parameters) 574 | parameters = defaults 575 | 576 | if token: 577 | parameters['oauth_token'] = token.key 578 | if token.verifier: 579 | parameters['oauth_verifier'] = token.verifier 580 | 581 | return cls(http_method, http_url, parameters, body=body, 582 | is_form_encoded=is_form_encoded) 583 | 584 | @classmethod 585 | def from_token_and_callback(cls, token, callback=None, 586 | http_method=HTTP_METHOD, http_url=None, parameters=None): 587 | 588 | if not parameters: 589 | parameters = {} 590 | 591 | parameters['oauth_token'] = token.key 592 | 593 | if callback: 594 | parameters['oauth_callback'] = callback 595 | 596 | return cls(http_method, http_url, parameters) 597 | 598 | @staticmethod 599 | def _split_header(header): 600 | """Turn Authorization: header into parameters.""" 601 | params = {} 602 | parts = header.split(',') 603 | for param in parts: 604 | # Ignore realm parameter. 605 | if param.find('realm') > -1: 606 | continue 607 | # Remove whitespace. 608 | param = param.strip() 609 | # Split key-value. 610 | param_parts = param.split('=', 1) 611 | # Remove quotes and unescape the value. 612 | params[param_parts[0]] = unquote(param_parts[1].strip('\"')) 613 | return params 614 | 615 | @staticmethod 616 | def _split_url_string(param_str): 617 | """Turn URL string into parameters.""" 618 | if not PY3: 619 | # If passed unicode with quoted UTF8, Python2's parse_qs leaves 620 | # mojibake'd uniocde after unquoting, so encode first. 621 | param_str = b(param_str, 'utf-8') 622 | parameters = parse_qs(param_str, keep_blank_values=True) 623 | for k, v in parameters.items(): 624 | if len(v) == 1: 625 | parameters[k] = unquote(v[0]) 626 | else: 627 | parameters[k] = sorted([unquote(s) for s in v]) 628 | return parameters 629 | 630 | 631 | class Client(httplib2.Http): 632 | """OAuthClient is a worker to attempt to execute a request.""" 633 | 634 | def __init__(self, consumer, token=None, **kwargs): 635 | 636 | if consumer is not None and not isinstance(consumer, Consumer): 637 | raise ValueError("Invalid consumer.") 638 | 639 | if token is not None and not isinstance(token, Token): 640 | raise ValueError("Invalid token.") 641 | 642 | self.consumer = consumer 643 | self.token = token 644 | self.method = SignatureMethod_HMAC_SHA1() 645 | 646 | super(Client, self).__init__(**kwargs) 647 | 648 | def set_signature_method(self, method): 649 | if not isinstance(method, SignatureMethod): 650 | raise ValueError("Invalid signature method.") 651 | 652 | self.method = method 653 | 654 | def request(self, uri, method="GET", body=b'', headers=None, 655 | redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None): 656 | DEFAULT_POST_CONTENT_TYPE = 'application/x-www-form-urlencoded' 657 | 658 | if not isinstance(headers, dict): 659 | headers = {} 660 | 661 | if method == "POST": 662 | headers['Content-Type'] = headers.get('Content-Type', 663 | DEFAULT_POST_CONTENT_TYPE) 664 | 665 | is_form_encoded = \ 666 | headers.get('Content-Type') == 'application/x-www-form-urlencoded' 667 | 668 | if is_form_encoded and body: 669 | parameters = parse_qs(body) 670 | else: 671 | parameters = None 672 | 673 | req = Request.from_consumer_and_token(self.consumer, 674 | token=self.token, http_method=method, http_url=uri, 675 | parameters=parameters, body=body, is_form_encoded=is_form_encoded) 676 | 677 | req.sign_request(self.method, self.consumer, self.token) 678 | 679 | scheme, netloc, path, params, query, fragment = urlparse(uri) 680 | realm = urlunparse((scheme, netloc, '', None, None, None)) 681 | 682 | if is_form_encoded: 683 | body = req.to_postdata() 684 | elif method == "GET": 685 | uri = req.to_url() 686 | else: 687 | headers.update(req.to_header(realm=realm)) 688 | 689 | return httplib2.Http.request(self, uri, method=method, body=body, 690 | headers=headers, redirections=redirections, 691 | connection_type=connection_type) 692 | 693 | 694 | class Server(object): 695 | """A skeletal implementation of a service provider, providing protected 696 | resources to requests from authorized consumers. 697 | 698 | This class implements the logic to check requests for authorization. You 699 | can use it with your web server or web framework to protect certain 700 | resources with OAuth. 701 | """ 702 | 703 | timestamp_threshold = 300 # In seconds, five minutes. 704 | version = OAUTH_VERSION 705 | signature_methods = None 706 | 707 | def __init__(self, signature_methods=None): 708 | self.signature_methods = signature_methods or {} 709 | 710 | def add_signature_method(self, signature_method): 711 | self.signature_methods[signature_method.name] = signature_method 712 | return self.signature_methods 713 | 714 | def verify_request(self, request, consumer, token): 715 | """Verifies an api call and checks all the parameters.""" 716 | 717 | self._check_version(request) 718 | self._check_signature(request, consumer, token) 719 | parameters = request.get_nonoauth_parameters() 720 | return parameters 721 | 722 | def build_authenticate_header(self, realm=''): 723 | """Optional support for the authenticate header.""" 724 | return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} 725 | 726 | def _check_version(self, request): 727 | """Verify the correct version of the request for this server.""" 728 | version = self._get_version(request) 729 | if version and version != self.version: 730 | raise Error('OAuth version %s not supported.' % str(version)) 731 | 732 | def _get_version(self, request): 733 | """Return the version of the request for this server.""" 734 | try: 735 | version = request.get_parameter('oauth_version') 736 | except: 737 | version = OAUTH_VERSION 738 | 739 | return version 740 | 741 | def _get_signature_method(self, request): 742 | """Figure out the signature with some defaults.""" 743 | signature_method = request.get('oauth_signature_method') 744 | if signature_method is None: 745 | signature_method = SIGNATURE_METHOD 746 | 747 | try: 748 | # Get the signature method object. 749 | return self.signature_methods[signature_method] 750 | except KeyError: 751 | signature_method_names = ', '.join(self.signature_methods.keys()) 752 | raise Error('Signature method %s not supported try one of the ' 753 | 'following: %s' 754 | % (signature_method, signature_method_names)) 755 | 756 | def _check_signature(self, request, consumer, token): 757 | timestamp, nonce = request._get_timestamp_nonce() 758 | self._check_timestamp(timestamp) 759 | signature_method = self._get_signature_method(request) 760 | 761 | signature = request.get('oauth_signature') 762 | if signature is None: 763 | raise MissingSignature('Missing oauth_signature.') 764 | if isinstance(signature, str): 765 | signature = signature.encode('ascii', 'ignore') 766 | 767 | # Validate the signature. 768 | valid = signature_method.check(request, consumer, token, signature) 769 | 770 | if not valid: 771 | key, base = signature_method.signing_base(request, consumer, token) 772 | 773 | raise Error('Invalid signature. Expected signature base ' 774 | 'string: %s' % base) 775 | 776 | def _check_timestamp(self, timestamp): 777 | """Verify that timestamp is recentish.""" 778 | timestamp = int(timestamp) 779 | now = int(time.time()) 780 | lapsed = now - timestamp 781 | if lapsed > self.timestamp_threshold: 782 | raise Error('Expired timestamp: given %d and now %s has a ' 783 | 'greater difference than threshold %d' % (timestamp, now, 784 | self.timestamp_threshold)) 785 | 786 | 787 | class SignatureMethod(object): 788 | """A way of signing requests. 789 | 790 | The OAuth protocol lets consumers and service providers pick a way to sign 791 | requests. This interface shows the methods expected by the other `oauth` 792 | modules for signing requests. Subclass it and implement its methods to 793 | provide a new way to sign requests. 794 | """ 795 | 796 | def signing_base(self, request, consumer, token): #pragma NO COVER 797 | """Calculates the string that needs to be signed. 798 | 799 | This method returns a 2-tuple containing the starting key for the 800 | signing and the message to be signed. The latter may be used in error 801 | messages to help clients debug their software. 802 | 803 | """ 804 | raise NotImplementedError 805 | 806 | def sign(self, request, consumer, token): #pragma NO COVER 807 | """Returns the signature for the given request, based on the consumer 808 | and token also provided. 809 | 810 | You should use your implementation of `signing_base()` to build the 811 | message to sign. Otherwise it may be less useful for debugging. 812 | 813 | """ 814 | raise NotImplementedError 815 | 816 | def check(self, request, consumer, token, signature): 817 | """Returns whether the given signature is the correct signature for 818 | the given consumer and token signing the given request.""" 819 | built = self.sign(request, consumer, token) 820 | return built == signature 821 | 822 | 823 | class SignatureMethod_HMAC_SHA1(SignatureMethod): 824 | name = 'HMAC-SHA1' 825 | 826 | def signing_base(self, request, consumer, token): 827 | if (not hasattr(request, 'normalized_url') or request.normalized_url is None): 828 | raise ValueError("Base URL for request is not set.") 829 | 830 | sig = ( 831 | escape(request.method), 832 | escape(request.normalized_url), 833 | escape(request.get_normalized_parameters()), 834 | ) 835 | 836 | key = '%s&' % escape(consumer.secret) 837 | if token: 838 | key += escape(token.secret) 839 | raw = '&'.join(sig) 840 | return key.encode('ascii'), raw.encode('ascii') 841 | 842 | def sign(self, request, consumer, token): 843 | """Builds the base signature string.""" 844 | key, raw = self.signing_base(request, consumer, token) 845 | 846 | hashed = hmac.new(key, raw, sha1) 847 | 848 | # Calculate the digest base 64. 849 | return binascii.b2a_base64(hashed.digest())[:-1] 850 | 851 | 852 | class SignatureMethod_PLAINTEXT(SignatureMethod): 853 | 854 | name = 'PLAINTEXT' 855 | 856 | def signing_base(self, request, consumer, token): 857 | """Concatenates the consumer key and secret with the token's 858 | secret.""" 859 | sig = '%s&' % escape(consumer.secret) 860 | if token: 861 | sig = sig + escape(token.secret) 862 | return sig, sig 863 | 864 | def sign(self, request, consumer, token): 865 | key, raw = self.signing_base(request, consumer, token) 866 | return raw.encode('utf8') 867 | -------------------------------------------------------------------------------- /tests/test_oauth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | The MIT License 5 | 6 | Copyright (c) 2009 Vic Fryzel 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | """ 26 | import sys 27 | import random 28 | import time 29 | import unittest 30 | 31 | import httplib2 32 | import mock 33 | 34 | from oauth2._compat import b 35 | from oauth2._compat import unquote 36 | from oauth2._compat import urlencode 37 | from oauth2._compat import urlparse 38 | from oauth2._compat import urlunparse 39 | from oauth2._compat import parse_qs 40 | from oauth2._compat import parse_qsl 41 | from oauth2._compat import u 42 | 43 | import oauth2 as oauth 44 | 45 | _UEMPTY = u('') 46 | _UBLANK = u(' ') 47 | _BSMILEY = b':-)' 48 | _USMILEY = u(_BSMILEY) 49 | _BGLYPH = b'\xae' 50 | _UGLYPH = u(_BGLYPH, 'latin1') 51 | _B2019 = b'\xe2\x80\x99' # u'\u2019' encoded to UTF-8 52 | _U2019 = u(_B2019, 'utf8') # u'\u2019' 53 | _B2766 = b'\xe2\x9d\xa6' # u'\u2766' encoded to UTF-8 54 | _U2766 = u(_B2766, 'utf8') # u'\u2766' 55 | 56 | PY3 = sys.version_info >= (3,) 57 | 58 | class TestError(unittest.TestCase): 59 | def test_message(self): 60 | try: 61 | raise oauth.Error 62 | except oauth.Error as e: 63 | self.assertEqual(e.message, 'OAuth error occurred.') 64 | 65 | msg = 'OMG THINGS BROKE!!!!' 66 | try: 67 | raise oauth.Error(msg) 68 | except oauth.Error as e: 69 | self.assertEqual(e.message, msg) 70 | 71 | def test_str(self): 72 | try: 73 | raise oauth.Error 74 | except oauth.Error as e: 75 | self.assertEqual(str(e), 'OAuth error occurred.') 76 | 77 | class TestGenerateFunctions(unittest.TestCase): 78 | def test_build_auth_header(self): 79 | header = oauth.build_authenticate_header() 80 | self.assertEqual(header['WWW-Authenticate'], 'OAuth realm=""') 81 | self.assertEqual(len(header), 1) 82 | realm = 'http://example.myrealm.com/' 83 | header = oauth.build_authenticate_header(realm) 84 | self.assertEqual(header['WWW-Authenticate'], 'OAuth realm="%s"' % 85 | realm) 86 | self.assertEqual(len(header), 1) 87 | 88 | def test_build_xoauth_string(self): 89 | consumer = oauth.Consumer('consumer_token', 'consumer_secret') 90 | token = oauth.Token('user_token', 'user_secret') 91 | url = "https://mail.google.com/mail/b/joe@example.com/imap/" 92 | xoauth_string = oauth.build_xoauth_string(url, consumer, token) 93 | 94 | method, oauth_url, oauth_string = xoauth_string.split(' ') 95 | 96 | self.assertEqual("GET", method) 97 | self.assertEqual(url, oauth_url) 98 | 99 | returned = {} 100 | parts = oauth_string.split(',') 101 | for part in parts: 102 | var, val = part.split('=') 103 | returned[var] = val.strip('"') 104 | 105 | self.assertEqual('HMAC-SHA1', returned['oauth_signature_method']) 106 | self.assertEqual('user_token', returned['oauth_token']) 107 | self.assertEqual('consumer_token', returned['oauth_consumer_key']) 108 | self.assertTrue('oauth_signature' in returned, 'oauth_signature') 109 | 110 | def test_escape(self): 111 | string = 'http://whatever.com/~someuser/?test=test&other=other' 112 | self.assertTrue('~' in oauth.escape(string)) 113 | string = '../../../../../../../etc/passwd' 114 | self.assertTrue('../' not in oauth.escape(string)) 115 | 116 | def test_gen_nonce(self): 117 | nonce = oauth.generate_nonce() 118 | self.assertEqual(len(nonce), 8) 119 | nonce = oauth.generate_nonce(20) 120 | self.assertEqual(len(nonce), 20) 121 | 122 | def test_gen_verifier(self): 123 | verifier = oauth.generate_verifier() 124 | self.assertEqual(len(verifier), 8) 125 | verifier = oauth.generate_verifier(16) 126 | self.assertEqual(len(verifier), 16) 127 | 128 | def test_gen_timestamp(self): 129 | exp = int(time.time()) 130 | now = oauth.generate_timestamp() 131 | self.assertEqual(exp, now) 132 | 133 | class TestConsumer(unittest.TestCase): 134 | def setUp(self): 135 | self.key = 'my-key' 136 | self.secret = 'my-secret' 137 | self.consumer = oauth.Consumer(key=self.key, secret=self.secret) 138 | 139 | def test_init(self): 140 | self.assertEqual(self.consumer.key, self.key) 141 | self.assertEqual(self.consumer.secret, self.secret) 142 | 143 | def test_basic(self): 144 | self.assertRaises(ValueError, lambda: oauth.Consumer(None, None)) 145 | self.assertRaises(ValueError, lambda: oauth.Consumer('asf', None)) 146 | self.assertRaises(ValueError, lambda: oauth.Consumer(None, 'dasf')) 147 | 148 | def test_str(self): 149 | res = dict(parse_qsl(str(self.consumer))) 150 | self.assertTrue('oauth_consumer_key' in res) 151 | self.assertTrue('oauth_consumer_secret' in res) 152 | self.assertEqual(res['oauth_consumer_key'], self.consumer.key) 153 | self.assertEqual(res['oauth_consumer_secret'], self.consumer.secret) 154 | 155 | class TestToken(unittest.TestCase): 156 | def setUp(self): 157 | self.key = 'my-key' 158 | self.secret = 'my-secret' 159 | self.token = oauth.Token(self.key, self.secret) 160 | 161 | def test_basic(self): 162 | self.assertRaises(ValueError, lambda: oauth.Token(None, None)) 163 | self.assertRaises(ValueError, lambda: oauth.Token('asf', None)) 164 | self.assertRaises(ValueError, lambda: oauth.Token(None, 'dasf')) 165 | 166 | def test_init(self): 167 | self.assertEqual(self.token.key, self.key) 168 | self.assertEqual(self.token.secret, self.secret) 169 | self.assertEqual(self.token.callback, None) 170 | self.assertEqual(self.token.callback_confirmed, None) 171 | self.assertEqual(self.token.verifier, None) 172 | 173 | def test_set_callback(self): 174 | self.assertEqual(self.token.callback, None) 175 | self.assertEqual(self.token.callback_confirmed, None) 176 | cb = 'http://www.example.com/my-callback' 177 | self.token.set_callback(cb) 178 | self.assertEqual(self.token.callback, cb) 179 | self.assertEqual(self.token.callback_confirmed, 'true') 180 | self.token.set_callback(None) 181 | self.assertEqual(self.token.callback, None) 182 | # TODO: The following test should probably not pass, but it does 183 | # To fix this, check for None and unset 'true' in set_callback 184 | # Additionally, should a confirmation truly be done of the callback? 185 | self.assertEqual(self.token.callback_confirmed, 'true') 186 | 187 | def test_set_verifier(self): 188 | self.assertEqual(self.token.verifier, None) 189 | v = oauth.generate_verifier() 190 | self.token.set_verifier(v) 191 | self.assertEqual(self.token.verifier, v) 192 | self.token.set_verifier() 193 | self.assertNotEqual(self.token.verifier, v) 194 | self.token.set_verifier('') 195 | self.assertEqual(self.token.verifier, '') 196 | 197 | def test_get_callback_url(self): 198 | self.assertEqual(self.token.get_callback_url(), None) 199 | 200 | self.token.set_verifier() 201 | self.assertEqual(self.token.get_callback_url(), None) 202 | 203 | cb = 'http://www.example.com/my-callback?save=1&return=true' 204 | v = oauth.generate_verifier() 205 | self.token.set_callback(cb) 206 | self.token.set_verifier(v) 207 | url = self.token.get_callback_url() 208 | verifier_str = '&oauth_verifier=%s' % v 209 | self.assertEqual(url, '%s%s' % (cb, verifier_str)) 210 | 211 | cb = 'http://www.example.com/my-callback-no-query' 212 | v = oauth.generate_verifier() 213 | self.token.set_callback(cb) 214 | self.token.set_verifier(v) 215 | url = self.token.get_callback_url() 216 | verifier_str = '?oauth_verifier=%s' % v 217 | self.assertEqual(url, '%s%s' % (cb, verifier_str)) 218 | 219 | def test_to_string(self): 220 | string = 'oauth_token=%s&oauth_token_secret=%s' % (self.key, self.secret) 221 | self.assertEqual(self.token.to_string(), string) 222 | 223 | self.token.set_callback('http://www.example.com/my-callback') 224 | string += '&oauth_callback_confirmed=true' 225 | self.assertEqual(self.token.to_string(), string) 226 | 227 | def _compare_tokens(self, new): 228 | self.assertEqual(self.token.key, new.key) 229 | self.assertEqual(self.token.secret, new.secret) 230 | # TODO: What about copying the callback to the new token? 231 | # self.assertEqual(self.token.callback, new.callback) 232 | self.assertEqual(self.token.callback_confirmed, 233 | new.callback_confirmed) 234 | # TODO: What about copying the verifier to the new token? 235 | # self.assertEqual(self.token.verifier, new.verifier) 236 | 237 | def test___str__(self): 238 | tok = oauth.Token('tooken', 'seecret') 239 | self.assertEqual(str(tok), 'oauth_token=tooken&oauth_token_secret=seecret') 240 | 241 | def test_from_string(self): 242 | self.assertRaises(ValueError, lambda: oauth.Token.from_string('')) 243 | self.assertRaises(ValueError, lambda: oauth.Token.from_string('blahblahblah')) 244 | self.assertRaises(ValueError, lambda: oauth.Token.from_string('blah=blah')) 245 | 246 | self.assertRaises(ValueError, lambda: oauth.Token.from_string('oauth_token_secret=asfdasf')) 247 | self.assertRaises(ValueError, lambda: oauth.Token.from_string('oauth_token_secret=')) 248 | self.assertRaises(ValueError, lambda: oauth.Token.from_string('oauth_token=asfdasf')) 249 | self.assertRaises(ValueError, lambda: oauth.Token.from_string('oauth_token=')) 250 | self.assertRaises(ValueError, lambda: oauth.Token.from_string('oauth_token=&oauth_token_secret=')) 251 | self.assertRaises(ValueError, lambda: oauth.Token.from_string('oauth_token=tooken%26oauth_token_secret=seecret')) 252 | 253 | string = self.token.to_string() 254 | new = oauth.Token.from_string(string) 255 | self._compare_tokens(new) 256 | 257 | self.token.set_callback('http://www.example.com/my-callback') 258 | string = self.token.to_string() 259 | new = oauth.Token.from_string(string) 260 | self._compare_tokens(new) 261 | 262 | class ReallyEqualMixin: 263 | def assertReallyEqual(self, a, b, msg=None): 264 | self.assertEqual(a, b, msg=msg) 265 | self.assertEqual(type(a), type(b), msg="a :: %r, b :: %r, %r" % (a, b, msg)) 266 | 267 | class TestFuncs(unittest.TestCase): 268 | def test_to_unicode(self): 269 | self.assertRaises(TypeError, oauth.to_unicode, 0) 270 | self.assertRaises(TypeError, oauth.to_unicode, b'\xae') 271 | self.assertRaises(TypeError, oauth.to_unicode_optional_iterator, b'\xae') 272 | self.assertRaises(TypeError, oauth.to_unicode_optional_iterator, [b'\xae']) 273 | 274 | self.assertEqual(oauth.to_unicode(_BSMILEY), _USMILEY) 275 | self.assertEqual(oauth.to_unicode(_UGLYPH), _UGLYPH) 276 | self.assertEqual(oauth.to_unicode(b'\xc2\xae'), _UGLYPH) 277 | 278 | def test_to_utf8(self): 279 | self.assertRaises(TypeError, oauth.to_utf8, 0) 280 | self.assertRaises(TypeError, oauth.to_utf8, b'\x81') 281 | self.assertEqual(oauth.to_utf8(_BSMILEY), _BSMILEY) 282 | self.assertEqual(oauth.to_utf8(_UGLYPH), 283 | _UGLYPH.encode('utf8')) 284 | 285 | def test_to_unicode_if_string(self): 286 | self.assertTrue(oauth.to_unicode_if_string(self) is self) 287 | self.assertEqual(oauth.to_unicode_if_string(_BSMILEY), _USMILEY) 288 | 289 | def test_to_utf8_if_string(self): 290 | self.assertTrue(oauth.to_utf8_if_string(self) is self) 291 | self.assertEqual(oauth.to_utf8_if_string(_USMILEY), _BSMILEY) 292 | self.assertEqual(oauth.to_utf8_if_string(_UGLYPH), 293 | _UGLYPH.encode('utf8')) 294 | 295 | def test_to_unicode_optional_iterator(self): 296 | self.assertEqual(oauth.to_unicode_optional_iterator(_BSMILEY), 297 | _USMILEY) 298 | self.assertEqual(oauth.to_unicode_optional_iterator(_UGLYPH), 299 | _UGLYPH) 300 | self.assertEqual(oauth.to_unicode_optional_iterator([_BSMILEY]), 301 | [_USMILEY]) 302 | self.assertEqual(oauth.to_unicode_optional_iterator([_UGLYPH]), 303 | [_UGLYPH]) 304 | self.assertEqual(oauth.to_unicode_optional_iterator((_UGLYPH,)), 305 | [_UGLYPH]) 306 | self.assertTrue(oauth.to_unicode_optional_iterator(self) is self) 307 | 308 | def test_to_utf8_optional_iterator(self): 309 | self.assertEqual(oauth.to_utf8_optional_iterator(_BSMILEY), 310 | _BSMILEY) 311 | self.assertEqual(oauth.to_utf8_optional_iterator(_UGLYPH), 312 | _UGLYPH.encode('utf8')) 313 | self.assertEqual(oauth.to_utf8_optional_iterator([_BSMILEY]), 314 | [_BSMILEY]) 315 | self.assertEqual(oauth.to_utf8_optional_iterator([_USMILEY]), 316 | [_BSMILEY]) 317 | self.assertEqual(oauth.to_utf8_optional_iterator([_UGLYPH]), 318 | [_UGLYPH.encode('utf8')]) 319 | self.assertEqual(oauth.to_utf8_optional_iterator((_UGLYPH,)), 320 | [_UGLYPH.encode('utf8')]) 321 | self.assertTrue(oauth.to_utf8_optional_iterator(self) is self) 322 | 323 | class TestRequest(unittest.TestCase, ReallyEqualMixin): 324 | def test__init__(self): 325 | method = "GET" 326 | req = oauth.Request(method) 327 | self.assertFalse('url' in req.__dict__) 328 | self.assertFalse('normalized_url' in req.__dict__) 329 | self.assertRaises(AttributeError, getattr, req, 'url') 330 | self.assertRaises(AttributeError, getattr, req, 'normalized_url') 331 | 332 | def test_setter(self): 333 | url = "http://example.com" 334 | method = "GET" 335 | req = oauth.Request(method, url) 336 | self.assertEqual(req.url, url) 337 | self.assertEqual(req.normalized_url, url) 338 | req.url = url + '/?foo=bar' 339 | self.assertEqual(req.url, url + '/?foo=bar') 340 | self.assertEqual(req.normalized_url, url + '/') 341 | req.url = None 342 | self.assertEqual(req.url, None) 343 | self.assertEqual(req.normalized_url, None) 344 | 345 | def test_deleter(self): 346 | url = "http://example.com" 347 | method = "GET" 348 | req = oauth.Request(method, url) 349 | del req.url 350 | self.assertRaises(AttributeError, getattr, req, 'url') 351 | 352 | def test_url(self): 353 | url1 = "http://example.com:80/foo.php" 354 | url2 = "https://example.com:443/foo.php" 355 | exp1 = "http://example.com/foo.php" 356 | exp2 = "https://example.com/foo.php" 357 | method = "GET" 358 | 359 | req = oauth.Request(method, url1) 360 | self.assertEqual(req.normalized_url, exp1) 361 | self.assertEqual(req.url, url1) 362 | 363 | req = oauth.Request(method, url2) 364 | self.assertEqual(req.normalized_url, exp2) 365 | self.assertEqual(req.url, url2) 366 | 367 | def test_bad_url(self): 368 | request = oauth.Request() 369 | try: 370 | request.url = "ftp://example.com" 371 | self.fail("Invalid URL scheme was accepted.") 372 | except ValueError: 373 | pass 374 | 375 | def test_unset_consumer_and_token(self): 376 | consumer = oauth.Consumer('my_consumer_key', 'my_consumer_secret') 377 | token = oauth.Token('my_key', 'my_secret') 378 | request = oauth.Request("GET", "http://example.com/fetch.php") 379 | request.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer, 380 | token) 381 | 382 | self.assertEqual(consumer.key, request['oauth_consumer_key']) 383 | self.assertEqual(token.key, request['oauth_token']) 384 | 385 | def test_no_url_set(self): 386 | consumer = oauth.Consumer('my_consumer_key', 'my_consumer_secret') 387 | token = oauth.Token('my_key', 'my_secret') 388 | request = oauth.Request() 389 | self.assertRaises(ValueError, 390 | request.sign_request, 391 | oauth.SignatureMethod_HMAC_SHA1(), consumer, token) 392 | 393 | def test_url_query(self): 394 | url = ("https://www.google.com/m8/feeds/contacts/default/full/?alt=json&max-contacts=10") 395 | normalized_url = urlunparse(urlparse(url)[:3] + (None, None, None)) 396 | method = "GET" 397 | req = oauth.Request(method, url) 398 | self.assertEqual(req.url, url) 399 | self.assertEqual(req.normalized_url, normalized_url) 400 | 401 | def test_get_parameter(self): 402 | url = "http://example.com" 403 | method = "GET" 404 | params = {'oauth_consumer' : 'asdf'} 405 | req = oauth.Request(method, url, parameters=params) 406 | 407 | self.assertEqual(req.get_parameter('oauth_consumer'), 'asdf') 408 | self.assertRaises(oauth.Error, req.get_parameter, 'blah') 409 | 410 | def test_get_nonoauth_parameters(self): 411 | 412 | oauth_params = { 413 | 'oauth_consumer': 'asdfasdfasdf' 414 | } 415 | 416 | other_params = { 417 | u('foo'): u('baz'), 418 | u('bar'): u('foo'), 419 | u('multi'): [u('FOO'), u('BAR')], 420 | u('uni_utf8'): u(b'\xae', 'latin1'), 421 | u('uni_unicode'): _UGLYPH, 422 | u('uni_unicode_2'): 423 | u(b'\xc3\xa5\xc3\x85\xc3\xb8\xc3\x98', 'latin1'), # 'åÅøØ' 424 | } 425 | 426 | params = oauth_params 427 | params.update(other_params) 428 | 429 | req = oauth.Request("GET", "http://example.com", params) 430 | self.assertEqual(other_params, req.get_nonoauth_parameters()) 431 | 432 | def test_to_url_nonascii(self): 433 | url = "http://sp.example.com/" 434 | 435 | params = { 436 | 'nonasciithing': u'q\xbfu\xe9 ,aasp u?..a.s', 437 | 'oauth_version': "1.0", 438 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 439 | 'oauth_timestamp': "137131200", 440 | 'oauth_consumer_key': "0685bd9184jfhq22", 441 | 'oauth_signature_method': "HMAC-SHA1", 442 | 'oauth_token': "ad180jjd733klru7", 443 | 'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", 444 | } 445 | 446 | req = oauth.Request("GET", url, params) 447 | res = urlparse(req.to_url()) 448 | 449 | params['nonasciithing'] = params['nonasciithing'].encode('utf-8') 450 | exp = urlparse("%s?%s" % (url, urlencode(params))) 451 | 452 | self.assertEquals(exp.netloc, res.netloc) 453 | self.assertEquals(exp.path, res.path) 454 | 455 | a = parse_qs(exp.query) 456 | b = parse_qs(res.query) 457 | self.assertEquals(a, b) 458 | 459 | def test_to_url_works_with_non_ascii_parameters(self): 460 | 461 | oauth_params = { 462 | 'oauth_consumer': 'asdfasdfasdf' 463 | } 464 | 465 | other_params = { 466 | u'foo': u'baz', 467 | u'bar': u'foo', 468 | u'uni_utf8': u'\xae', 469 | u'uni_unicode': u'\u00ae', 470 | u'uni_unicode_2': u'åÅøØ', 471 | } 472 | 473 | params = oauth_params 474 | params.update(other_params) 475 | 476 | req = oauth.Request("GET", "http://example.com", params) 477 | 478 | # We need to split out the host and params and check individually since the order is not determinate. 479 | url_parts = req.to_url().split("?") 480 | host = url_parts[0] 481 | params = dict(item.strip().split("=") for item in url_parts[1].split("&")) 482 | 483 | expected_params = { 484 | 'uni_utf8': '%C2%AE', 485 | 'foo': 'baz', 486 | 'bar': 'foo', 487 | 'uni_unicode_2': '%C3%A5%C3%85%C3%B8%C3%98', 488 | 'uni_unicode': '%C2%AE', 489 | 'oauth_consumer': 'asdfasdfasdf' 490 | } 491 | 492 | self.assertEquals("http://example.com", host) 493 | self.assertEquals(expected_params, params) 494 | 495 | def test_to_header(self): 496 | realm = "http://sp.example.com/" 497 | 498 | params = { 499 | 'oauth_version': "1.0", 500 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 501 | 'oauth_timestamp': "137131200", 502 | 'oauth_consumer_key': "0685bd9184jfhq22", 503 | 'oauth_signature_method': "HMAC-SHA1", 504 | 'oauth_token': "ad180jjd733klru7", 505 | 'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", 506 | } 507 | 508 | req = oauth.Request("GET", realm, params) 509 | header, value = list(req.to_header(realm).items())[0] 510 | 511 | parts = value.split('OAuth ') 512 | vars = parts[1].split(', ') 513 | self.assertTrue(len(vars), (len(params) + 1)) 514 | 515 | res = {} 516 | for v in vars: 517 | var, val = v.split('=') 518 | res[var] = unquote(val.strip('"')) 519 | 520 | self.assertEqual(realm, res['realm']) 521 | del res['realm'] 522 | 523 | self.assertTrue(len(res), len(params)) 524 | 525 | for key, val in res.items(): 526 | self.assertEqual(val, params.get(key)) 527 | 528 | def test_to_postdata_nonascii(self): 529 | realm = "http://sp.example.com/" 530 | 531 | params = { 532 | 'nonasciithing': u('q\xbfu\xe9 ,aasp u?..a.s', 'latin1'), 533 | 'oauth_version': "1.0", 534 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 535 | 'oauth_timestamp': "137131200", 536 | 'oauth_consumer_key': "0685bd9184jfhq22", 537 | 'oauth_signature_method': "HMAC-SHA1", 538 | 'oauth_token': "ad180jjd733klru7", 539 | 'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", 540 | } 541 | 542 | req = oauth.Request("GET", realm, params) 543 | 544 | self.assertReallyEqual( 545 | req.to_postdata(), 546 | ('nonasciithing=q%C2%BFu%C3%A9%20%2Caasp%20u%3F..a.s' 547 | '&oauth_consumer_key=0685bd9184jfhq22' 548 | '&oauth_nonce=4572616e48616d6d65724c61686176' 549 | '&oauth_signature=wOJIO9A2W5mFwDgiDvZbTSMK%252FPY%253D' 550 | '&oauth_signature_method=HMAC-SHA1' 551 | '&oauth_timestamp=137131200' 552 | '&oauth_token=ad180jjd733klru7' 553 | '&oauth_version=1.0' 554 | )) 555 | 556 | def test_to_postdata(self): 557 | realm = "http://sp.example.com/" 558 | 559 | params = { 560 | 'multi': ['FOO','BAR'], 561 | 'oauth_version': "1.0", 562 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 563 | 'oauth_timestamp': "137131200", 564 | 'oauth_consumer_key': "0685bd9184jfhq22", 565 | 'oauth_signature_method': "HMAC-SHA1", 566 | 'oauth_token': "ad180jjd733klru7", 567 | 'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", 568 | } 569 | 570 | req = oauth.Request("GET", realm, params) 571 | 572 | flat = [('multi','FOO'),('multi','BAR')] 573 | del params['multi'] 574 | flat.extend(params.items()) 575 | kf = lambda x: x[0] 576 | self.assertEqual( 577 | sorted(flat, key=kf), 578 | sorted(parse_qsl(req.to_postdata()), key=kf)) 579 | 580 | def test_to_url(self): 581 | url = "http://sp.example.com/" 582 | 583 | params = { 584 | 'oauth_version': "1.0", 585 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 586 | 'oauth_timestamp': "137131200", 587 | 'oauth_consumer_key': "0685bd9184jfhq22", 588 | 'oauth_signature_method': "HMAC-SHA1", 589 | 'oauth_token': "ad180jjd733klru7", 590 | 'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", 591 | } 592 | 593 | req = oauth.Request("GET", url, params) 594 | exp = urlparse("%s?%s" % (url, urlencode(params))) 595 | res = urlparse(req.to_url()) 596 | self.assertEqual(exp.scheme, res.scheme) 597 | self.assertEqual(exp.netloc, res.netloc) 598 | self.assertEqual(exp.path, res.path) 599 | 600 | exp_parsed = parse_qs(exp.query) 601 | res_parsed = parse_qs(res.query) 602 | self.assertEqual(exp_parsed, res_parsed) 603 | 604 | def test_to_url_with_query(self): 605 | url = ("https://www.google.com/m8/feeds/contacts/default/full/" 606 | "?alt=json&max-contacts=10") 607 | 608 | params = { 609 | 'oauth_version': "1.0", 610 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 611 | 'oauth_timestamp': "137131200", 612 | 'oauth_consumer_key': "0685bd9184jfhq22", 613 | 'oauth_signature_method': "HMAC-SHA1", 614 | 'oauth_token': "ad180jjd733klru7", 615 | 'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", 616 | } 617 | 618 | req = oauth.Request("GET", url, params) 619 | # Note: the url above already has query parameters, so append new 620 | # ones with & 621 | exp = urlparse("%s&%s" % (url, urlencode(params))) 622 | res = urlparse(req.to_url()) 623 | self.assertEqual(exp.scheme, res.scheme) 624 | self.assertEqual(exp.netloc, res.netloc) 625 | self.assertEqual(exp.path, res.path) 626 | 627 | exp_q = parse_qs(exp.query) 628 | res_q = parse_qs(res.query) 629 | self.assertTrue('alt' in res_q) 630 | self.assertTrue('max-contacts' in res_q) 631 | self.assertEqual(res_q['alt'], ['json']) 632 | self.assertEqual(res_q['max-contacts'], ['10']) 633 | self.assertEqual(exp_q, res_q) 634 | 635 | def test_signature_base_unicode_nonascii(self): 636 | consumer = oauth.Consumer('consumer_token', 'consumer_secret') 637 | 638 | url = u('http://api.simplegeo.com:80/1.0/places/address.json' 639 | '?q=monkeys&category=animal' 640 | '&address=41+Decatur+St,+San+Francisc') + _U2766 + u(',+CA') 641 | req = oauth.Request("GET", url) 642 | self.assertReallyEqual( 643 | req.normalized_url, 644 | u('http://api.simplegeo.com/1.0/places/address.json')) 645 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer, None) 646 | self.assertReallyEqual( 647 | req['oauth_signature'], b'WhufgeZKyYpKsI70GZaiDaYwl6g=') 648 | 649 | def test_signature_base_string_bytes_nonascii_nonutf8(self): 650 | consumer = oauth.Consumer('consumer_token', 'consumer_secret') 651 | 652 | url = (b'http://api.simplegeo.com:80/1.0/places/address.json' 653 | b'?q=monkeys&category=animal' 654 | b'&address=41+Decatur+St,+San+Francisc') + _B2766 + b',+CA' 655 | req = oauth.Request("GET", url) 656 | self.assertReallyEqual( 657 | req.normalized_url, 658 | u('http://api.simplegeo.com/1.0/places/address.json')) 659 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer, None) 660 | self.assertReallyEqual( #XXX 661 | req['oauth_signature'], b'WhufgeZKyYpKsI70GZaiDaYwl6g=') 662 | 663 | def test_signature_base_bytes_nonascii_nonutf8_urlencoded(self): 664 | consumer = oauth.Consumer('consumer_token', 'consumer_secret') 665 | 666 | url = (b'http://api.simplegeo.com:80/1.0/places/address.json' 667 | b'?q=monkeys&category=animal' 668 | b'&address=41+Decatur+St,+San+Francisc%E2%9D%A6,+CA') 669 | req = oauth.Request("GET", url) 670 | self.assertReallyEqual( 671 | req.normalized_url, 672 | u('http://api.simplegeo.com/1.0/places/address.json')) 673 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer, None) 674 | self.assertReallyEqual( 675 | req['oauth_signature'], b'WhufgeZKyYpKsI70GZaiDaYwl6g=') 676 | 677 | def test_signature_base_unicode_nonascii_nonutf8_url_encoded(self): 678 | consumer = oauth.Consumer('consumer_token', 'consumer_secret') 679 | 680 | url = u('http://api.simplegeo.com:80/1.0/places/address.json' 681 | '?q=monkeys&category=animal' 682 | '&address=41+Decatur+St,+San+Francisc%E2%9D%A6,+CA') 683 | req = oauth.Request("GET", url) 684 | self.assertReallyEqual( 685 | req.normalized_url, 686 | u('http://api.simplegeo.com/1.0/places/address.json')) 687 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer, None) 688 | self.assertReallyEqual( 689 | req['oauth_signature'], b'WhufgeZKyYpKsI70GZaiDaYwl6g=') 690 | 691 | def test_signature_base_string_with_query(self): 692 | url = ("https://www.google.com/m8/feeds/contacts/default/full/" 693 | "?alt=json&max-contacts=10") 694 | params = { 695 | 'oauth_version': "1.0", 696 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 697 | 'oauth_timestamp': "137131200", 698 | 'oauth_consumer_key': "0685bd9184jfhq22", 699 | 'oauth_signature_method': "HMAC-SHA1", 700 | 'oauth_token': "ad180jjd733klru7", 701 | 'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", 702 | } 703 | req = oauth.Request("GET", url, params) 704 | self.assertEqual( 705 | req.normalized_url, 706 | 'https://www.google.com/m8/feeds/contacts/default/full/') 707 | self.assertEqual(req.url, url) 708 | normalized_params = parse_qsl(req.get_normalized_parameters()) 709 | self.assertTrue(len(normalized_params), len(params) + 2) 710 | normalized_params = dict(normalized_params) 711 | for key, value in params.items(): 712 | if key == 'oauth_signature': 713 | continue 714 | self.assertEqual(value, normalized_params[key]) 715 | self.assertEqual(normalized_params['alt'], 'json') 716 | self.assertEqual(normalized_params['max-contacts'], '10') 717 | 718 | def test_get_normalized_parameters_empty(self): 719 | url = "http://sp.example.com/?empty=" 720 | 721 | req = oauth.Request("GET", url) 722 | 723 | res = req.get_normalized_parameters() 724 | 725 | expected='empty=' 726 | 727 | self.assertEqual(expected, res) 728 | 729 | def test_get_normalized_parameters_duplicate(self): 730 | url = ("http://example.com/v2/search/videos" 731 | "?oauth_nonce=79815175&oauth_timestamp=1295397962" 732 | "&oauth_consumer_key=mykey&oauth_signature_method=HMAC-SHA1" 733 | "&q=car&oauth_version=1.0&offset=10" 734 | "&oauth_signature=spWLI%2FGQjid7sQVd5%2FarahRxzJg%3D") 735 | 736 | req = oauth.Request("GET", url) 737 | 738 | res = req.get_normalized_parameters() 739 | 740 | expected = ('oauth_consumer_key=mykey&oauth_nonce=79815175' 741 | '&oauth_signature_method=HMAC-SHA1' 742 | '&oauth_timestamp=1295397962&oauth_version=1.0' 743 | '&offset=10&q=car') 744 | 745 | self.assertEqual(expected, res) 746 | 747 | def test_get_normalized_parameters_multiple(self): 748 | url = "http://example.com/v2/search/videos?oauth_nonce=79815175&oauth_timestamp=1295397962&oauth_consumer_key=mykey&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&offset=10&oauth_signature=spWLI%2FGQjid7sQVd5%2FarahRxzJg%3D&tag=one&tag=two" 749 | 750 | req = oauth.Request("GET", url) 751 | 752 | res = req.get_normalized_parameters() 753 | 754 | expected='oauth_consumer_key=mykey&oauth_nonce=79815175&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1295397962&oauth_version=1.0&offset=10&tag=one&tag=two' 755 | 756 | self.assertEqual(expected, res) 757 | 758 | 759 | def test_get_normalized_parameters_from_url(self): 760 | # example copied from 761 | # https://github.com/ciaranj/node-oauth/blob/master/tests/oauth.js 762 | # which in turns says that it was copied from 763 | # http://oauth.net/core/1.0/#sig_base_example . 764 | url = ("http://photos.example.net/photos?file=vacation.jpg" 765 | "&oauth_consumer_key=dpf43f3p2l4k3l03" 766 | "&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1" 767 | "&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk" 768 | "&oauth_version=1.0&size=original") 769 | 770 | req = oauth.Request("GET", url) 771 | 772 | res = req.get_normalized_parameters() 773 | 774 | expected = ('file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03' 775 | '&oauth_nonce=kllo9940pd9333jh' 776 | '&oauth_signature_method=HMAC-SHA1' 777 | '&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk' 778 | '&oauth_version=1.0&size=original') 779 | 780 | self.assertEqual(expected, res) 781 | 782 | def test_signing_base(self): 783 | # example copied from 784 | # https://github.com/ciaranj/node-oauth/blob/master/tests/oauth.js 785 | # which in turns says that it was copied from 786 | # http://oauth.net/core/1.0/#sig_base_example . 787 | url = ("http://photos.example.net/photos?file=vacation.jpg" 788 | "&oauth_consumer_key=dpf43f3p2l4k3l03" 789 | "&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1" 790 | "&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk" 791 | "&oauth_version=1.0&size=original") 792 | 793 | req = oauth.Request("GET", url) 794 | 795 | sm = oauth.SignatureMethod_HMAC_SHA1() 796 | 797 | consumer = oauth.Consumer('dpf43f3p2l4k3l03', 'foo') 798 | key, raw = sm.signing_base(req, consumer, None) 799 | 800 | expected = b('GET&http%3A%2F%2Fphotos.example.net%2Fphotos' 801 | '&file%3Dvacation.jpg' 802 | '%26oauth_consumer_key%3Ddpf43f3p2l4k3l03' 803 | '%26oauth_nonce%3Dkllo9940pd9333jh' 804 | '%26oauth_signature_method%3DHMAC-SHA1' 805 | '%26oauth_timestamp%3D1191242096' 806 | '%26oauth_token%3Dnnch734d00sl2jdk' 807 | '%26oauth_version%3D1.0%26size%3Doriginal') 808 | self.assertEqual(expected, raw) 809 | 810 | def test_get_normalized_parameters(self): 811 | url = "http://sp.example.com/" 812 | 813 | params = { 814 | 'oauth_version': "1.0", 815 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 816 | 'oauth_timestamp': "137131200", 817 | 'oauth_consumer_key': "0685bd9184jfhq22", 818 | 'oauth_signature_method': "HMAC-SHA1", 819 | 'oauth_token': "ad180jjd733klru7", 820 | 'multi': ['FOO','BAR', _UGLYPH, b'\xc2\xae'], 821 | 'multi_same': ['FOO','FOO'], 822 | 'uni_utf8_bytes': b'\xc2\xae', 823 | 'uni_unicode_object': _UGLYPH 824 | } 825 | 826 | req = oauth.Request("GET", url, params) 827 | 828 | res = req.get_normalized_parameters() 829 | 830 | expected = ('multi=BAR&multi=FOO&multi=%C2%AE&multi=%C2%AE' 831 | '&multi_same=FOO&multi_same=FOO' 832 | '&oauth_consumer_key=0685bd9184jfhq22' 833 | '&oauth_nonce=4572616e48616d6d65724c61686176' 834 | '&oauth_signature_method=HMAC-SHA1' 835 | '&oauth_timestamp=137131200' 836 | '&oauth_token=ad180jjd733klru7' 837 | '&oauth_version=1.0' 838 | '&uni_unicode_object=%C2%AE&uni_utf8_bytes=%C2%AE') 839 | 840 | self.assertEqual(expected, res) 841 | 842 | def test_get_normalized_parameters_ignores_auth_signature(self): 843 | url = "http://sp.example.com/" 844 | 845 | params = { 846 | 'oauth_version': "1.0", 847 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 848 | 'oauth_timestamp': "137131200", 849 | 'oauth_consumer_key': "0685bd9184jfhq22", 850 | 'oauth_signature_method': "HMAC-SHA1", 851 | 'oauth_signature': "some-random-signature-%d" % random.randint(1000, 2000), 852 | 'oauth_token': "ad180jjd733klru7", 853 | } 854 | 855 | req = oauth.Request("GET", url, params) 856 | 857 | res = req.get_normalized_parameters() 858 | 859 | self.assertNotEqual(urlencode(sorted(params.items())), res) 860 | 861 | foo = params.copy() 862 | del foo["oauth_signature"] 863 | self.assertEqual(urlencode(sorted(foo.items())), res) 864 | 865 | def test_signature_base_string_with_matrix_params(self): 866 | url = "http://social.yahooapis.com/v1/user/6677/connections;start=0;count=20" 867 | req = oauth.Request("GET", url, None) 868 | self.assertEqual(req.normalized_url, 'http://social.yahooapis.com/v1/user/6677/connections;start=0;count=20') 869 | self.assertEqual(req.url, 'http://social.yahooapis.com/v1/user/6677/connections;start=0;count=20') 870 | 871 | def test_set_signature_method(self): 872 | consumer = oauth.Consumer('key', 'secret') 873 | client = oauth.Client(consumer) 874 | 875 | class Blah: 876 | pass 877 | 878 | try: 879 | client.set_signature_method(Blah()) 880 | self.fail("Client.set_signature_method() accepted invalid method.") 881 | except ValueError: 882 | pass 883 | 884 | m = oauth.SignatureMethod_HMAC_SHA1() 885 | client.set_signature_method(m) 886 | self.assertEqual(m, client.method) 887 | 888 | def test_get_normalized_string_escapes_spaces_properly(self): 889 | url = "http://sp.example.com/" 890 | params = { 891 | "some_random_data": random.randint(100, 1000), 892 | "data": "This data with a random number (%d) has spaces!" 893 | % random.randint(1000, 2000), 894 | } 895 | 896 | req = oauth.Request("GET", url, params) 897 | res = req.get_normalized_parameters() 898 | expected = urlencode(sorted(params.items())).replace('+', '%20') 899 | self.assertEqual(expected, res) 900 | 901 | @mock.patch('oauth2.Request.make_timestamp') 902 | @mock.patch('oauth2.Request.make_nonce') 903 | def test_request_nonutf8_bytes(self, mock_make_nonce, mock_make_timestamp): 904 | mock_make_nonce.return_value = 5 905 | mock_make_timestamp.return_value = 6 906 | 907 | tok = oauth.Token(key="tok-test-key", secret="tok-test-secret") 908 | con = oauth.Consumer(key="con-test-key", secret="con-test-secret") 909 | params = { 910 | 'oauth_version': "1.0", 911 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 912 | 'oauth_timestamp': "137131200", 913 | 'oauth_token': tok.key, 914 | 'oauth_consumer_key': con.key 915 | } 916 | 917 | if not PY3: 918 | # If someone passes a sequence of bytes which is not ascii for 919 | # url, we'll raise an exception as early as possible. 920 | url = "http://sp.example.com/\x92" # It's actually cp1252-encoding... 921 | self.assertRaises(TypeError, oauth.Request, method="GET", url=url, parameters=params) 922 | 923 | # And if they pass an unicode, then we'll use it. 924 | url = u('http://sp.example.com/') + _U2019 925 | req = oauth.Request(method="GET", url=url, parameters=params) 926 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None) 927 | self.assertReallyEqual(req['oauth_signature'], b'cMzvCkhvLL57+sTIxLITTHfkqZk=') 928 | 929 | # And if it is a utf-8-encoded-then-percent-encoded non-ascii 930 | # thing, we'll decode it and use it. 931 | url = "http://sp.example.com/%E2%80%99" 932 | req = oauth.Request(method="GET", url=url, parameters=params) 933 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None) 934 | self.assertReallyEqual(req['oauth_signature'], b'yMLKOyNKC/DkyhUOb8DLSvceEWE=') 935 | 936 | # Same thing with the params. 937 | url = "http://sp.example.com/" 938 | 939 | # If someone passes a sequence of bytes which is not ascii in 940 | # params, we'll raise an exception as early as possible. 941 | params['non_oauth_thing'] = b'\xae', # It's actually cp1252-encoding... 942 | self.assertRaises(TypeError, oauth.Request, method="GET", url=url, parameters=params) 943 | 944 | # And if they pass a unicode, then we'll use it. 945 | params['non_oauth_thing'] = _U2019 946 | req = oauth.Request(method="GET", url=url, parameters=params) 947 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None) 948 | self.assertReallyEqual(req['oauth_signature'], b'0GU50m0v60CVDB5JnoBXnvvvKx4=') 949 | 950 | # And if it is a utf-8-encoded non-ascii thing, we'll decode 951 | # it and use it. 952 | params['non_oauth_thing'] = b'\xc2\xae' 953 | req = oauth.Request(method="GET", url=url, parameters=params) 954 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None) 955 | self.assertReallyEqual(req['oauth_signature'], b'pqOCu4qvRTiGiXB8Z61Jsey0pMM=') 956 | 957 | 958 | # Also if there are non-utf8 bytes in the query args. 959 | url = b"http://sp.example.com/?q=\x92" # cp1252 960 | self.assertRaises(TypeError, oauth.Request, method="GET", url=url, parameters=params) 961 | 962 | def test_request_hash_of_body(self): 963 | tok = oauth.Token(key="token", secret="tok-test-secret") 964 | con = oauth.Consumer(key="consumer", secret="con-test-secret") 965 | 966 | # Example 1a from Appendix A.1 of 967 | # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html 968 | # Except that we get a differetn result than they do. 969 | 970 | params = { 971 | 'oauth_version': "1.0", 972 | 'oauth_token': tok.key, 973 | 'oauth_nonce': 10288510250934, 974 | 'oauth_timestamp': 1236874155, 975 | 'oauth_consumer_key': con.key 976 | } 977 | 978 | url = u('http://www.example.com/resource') 979 | req = oauth.Request(method="PUT", url=url, parameters=params, body=b"Hello World!", is_form_encoded=False) 980 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None) 981 | self.assertReallyEqual(req['oauth_body_hash'], b'Lve95gjOVATpfV8EL5X4nxwjKHE=') 982 | self.assertReallyEqual(req['oauth_signature'], b't+MX8l/0S8hdbVQL99nD0X1fPnM=') 983 | # oauth-bodyhash.html A.1 has 984 | # '08bUFF%2Fjmp59mWB7cSgCYBUpJ0U%3D', but I don't see how that 985 | # is possible. 986 | 987 | # Example 1b 988 | params = { 989 | 'oauth_version': "1.0", 990 | 'oauth_token': tok.key, 991 | 'oauth_nonce': 10369470270925, 992 | 'oauth_timestamp': 1236874236, 993 | 'oauth_consumer_key': con.key 994 | } 995 | 996 | req = oauth.Request(method="PUT", url=url, parameters=params, body=b"Hello World!", is_form_encoded=False) 997 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None) 998 | self.assertReallyEqual(req['oauth_body_hash'], b'Lve95gjOVATpfV8EL5X4nxwjKHE=') 999 | self.assertReallyEqual(req['oauth_signature'], b'CTFmrqJIGT7NsWJ42OrujahTtTc=') 1000 | 1001 | # Appendix A.2 1002 | params = { 1003 | 'oauth_version': "1.0", 1004 | 'oauth_token': tok.key, 1005 | 'oauth_nonce': 8628868109991, 1006 | 'oauth_timestamp': 1238395022, 1007 | 'oauth_consumer_key': con.key 1008 | } 1009 | 1010 | req = oauth.Request(method="GET", url=url, parameters=params, is_form_encoded=False) 1011 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None) 1012 | self.assertReallyEqual(req['oauth_body_hash'], b'2jmj7l5rSw0yVb/vlWAYkK/YBwk=') 1013 | self.assertReallyEqual(req['oauth_signature'], b'Zhl++aWSP0O3/hYQ0CuBc7jv38I=') 1014 | 1015 | 1016 | def test_sign_request(self): 1017 | url = "http://sp.example.com/" 1018 | 1019 | params = { 1020 | 'oauth_version': "1.0", 1021 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 1022 | 'oauth_timestamp': "137131200" 1023 | } 1024 | 1025 | tok = oauth.Token(key="tok-test-key", secret="tok-test-secret") 1026 | con = oauth.Consumer(key="con-test-key", secret="con-test-secret") 1027 | 1028 | params['oauth_token'] = tok.key 1029 | params['oauth_consumer_key'] = con.key 1030 | req = oauth.Request(method="GET", url=url, parameters=params) 1031 | 1032 | methods = { 1033 | b'DX01TdHws7OninCLK9VztNTH1M4=': oauth.SignatureMethod_HMAC_SHA1(), 1034 | b'con-test-secret&tok-test-secret': oauth.SignatureMethod_PLAINTEXT() 1035 | } 1036 | 1037 | for exp, method in methods.items(): 1038 | req.sign_request(method, con, tok) 1039 | self.assertEqual(req['oauth_signature_method'], method.name) 1040 | self.assertEqual(req['oauth_signature'], exp) 1041 | 1042 | # Also if there are non-ascii chars in the URL. 1043 | url = b"http://sp.example.com/\xe2\x80\x99" # utf-8 bytes 1044 | req = oauth.Request(method="GET", url=url, parameters=params) 1045 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, tok) 1046 | self.assertEqual(req['oauth_signature'], b'loFvp5xC7YbOgd9exIO6TxB7H4s=') 1047 | 1048 | url = u('http://sp.example.com/') + _U2019 # Python unicode object 1049 | req = oauth.Request(method="GET", url=url, parameters=params) 1050 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, tok) 1051 | self.assertEqual(req['oauth_signature'], b'loFvp5xC7YbOgd9exIO6TxB7H4s=') 1052 | 1053 | # Also if there are non-ascii chars in the query args. 1054 | url = b"http://sp.example.com/?q=\xe2\x80\x99" # utf-8 bytes 1055 | req = oauth.Request(method="GET", url=url, parameters=params) 1056 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, tok) 1057 | self.assertEqual(req['oauth_signature'], b'IBw5mfvoCsDjgpcsVKbyvsDqQaU=') 1058 | 1059 | url = u('http://sp.example.com/?q=') + _U2019 # Python unicode object 1060 | req = oauth.Request(method="GET", url=url, parameters=params) 1061 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, tok) 1062 | self.assertEqual(req['oauth_signature'], b'IBw5mfvoCsDjgpcsVKbyvsDqQaU=') 1063 | 1064 | 1065 | def test_from_request_works_with_wsgi(self): 1066 | """Make sure WSGI header HTTP_AUTHORIZATION is detected correctly.""" 1067 | url = "http://sp.example.com/" 1068 | 1069 | params = { 1070 | 'oauth_version': "1.0", 1071 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 1072 | 'oauth_timestamp': "137131200", 1073 | 'oauth_consumer_key': "0685bd9184jfhq22", 1074 | 'oauth_signature_method': "HMAC-SHA1", 1075 | 'oauth_token': "ad180jjd733klru7", 1076 | 'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", 1077 | } 1078 | 1079 | req = oauth.Request("GET", url, params) 1080 | headers = req.to_header() 1081 | 1082 | # Munge the headers 1083 | headers['HTTP_AUTHORIZATION'] = headers['Authorization'] 1084 | del headers['Authorization'] 1085 | 1086 | # Test from the headers 1087 | req = oauth.Request.from_request("GET", url, headers) 1088 | self.assertEqual(req.method, "GET") 1089 | self.assertEqual(req.url, url) 1090 | self.assertEqual(params, req.copy()) 1091 | 1092 | 1093 | def test_from_request_is_case_insensitive_checking_for_auth(self): 1094 | """Checks for the Authorization header should be case insensitive.""" 1095 | url = "http://sp.example.com/" 1096 | 1097 | params = { 1098 | 'oauth_version': "1.0", 1099 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 1100 | 'oauth_timestamp': "137131200", 1101 | 'oauth_consumer_key': "0685bd9184jfhq22", 1102 | 'oauth_signature_method': "HMAC-SHA1", 1103 | 'oauth_token': "ad180jjd733klru7", 1104 | 'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", 1105 | } 1106 | 1107 | req = oauth.Request("GET", url, params) 1108 | headers = req.to_header() 1109 | 1110 | # Munge the headers 1111 | headers['authorization'] = headers['Authorization'] 1112 | del headers['Authorization'] 1113 | 1114 | # Test from the headers 1115 | req = oauth.Request.from_request("GET", url, headers) 1116 | self.assertEqual(req.method, "GET") 1117 | self.assertEqual(req.url, url) 1118 | self.assertEqual(params, req.copy()) 1119 | 1120 | 1121 | def test_from_request(self): 1122 | url = "http://sp.example.com/" 1123 | 1124 | params = { 1125 | 'oauth_version': "1.0", 1126 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 1127 | 'oauth_timestamp': "137131200", 1128 | 'oauth_consumer_key': "0685bd9184jfhq22", 1129 | 'oauth_signature_method': "HMAC-SHA1", 1130 | 'oauth_token': "ad180jjd733klru7", 1131 | 'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", 1132 | } 1133 | 1134 | req = oauth.Request("GET", url, params) 1135 | headers = req.to_header() 1136 | 1137 | # Test from the headers 1138 | req = oauth.Request.from_request("GET", url, headers) 1139 | self.assertEqual(req.method, "GET") 1140 | self.assertEqual(req.url, url) 1141 | 1142 | self.assertEqual(params, req.copy()) 1143 | 1144 | # Test with bad OAuth headers 1145 | bad_headers = { 1146 | 'Authorization' : 'OAuth this is a bad header' 1147 | } 1148 | 1149 | self.assertRaises(oauth.Error, oauth.Request.from_request, "GET", 1150 | url, bad_headers) 1151 | 1152 | # Test getting from query string 1153 | qs = urlencode(params) 1154 | req = oauth.Request.from_request("GET", url, query_string=qs) 1155 | 1156 | exp = parse_qs(qs, keep_blank_values=False) 1157 | for k, v in exp.items(): 1158 | exp[k] = unquote(v[0]) 1159 | 1160 | self.assertEqual(exp, req.copy()) 1161 | 1162 | # Test that a boned from_request() call returns None 1163 | req = oauth.Request.from_request("GET", url) 1164 | self.assertEqual(None, req) 1165 | 1166 | def test_from_token_and_callback(self): 1167 | url = "http://sp.example.com/" 1168 | 1169 | params = { 1170 | 'oauth_version': "1.0", 1171 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 1172 | 'oauth_timestamp': "137131200", 1173 | 'oauth_consumer_key': "0685bd9184jfhq22", 1174 | 'oauth_signature_method': "HMAC-SHA1", 1175 | 'oauth_token': "ad180jjd733klru7", 1176 | 'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", 1177 | } 1178 | 1179 | tok = oauth.Token(key="tok-test-key", secret="tok-test-secret") 1180 | req = oauth.Request.from_token_and_callback(tok) 1181 | self.assertFalse('oauth_callback' in req) 1182 | self.assertEqual(req['oauth_token'], tok.key) 1183 | 1184 | req = oauth.Request.from_token_and_callback(tok, callback=url) 1185 | self.assertTrue('oauth_callback' in req) 1186 | self.assertEqual(req['oauth_callback'], url) 1187 | 1188 | def test_from_consumer_and_token(self): 1189 | url = "http://sp.example.com/" 1190 | 1191 | tok = oauth.Token(key="tok-test-key", secret="tok-test-secret") 1192 | tok.set_verifier('this_is_a_test_verifier') 1193 | con = oauth.Consumer(key="con-test-key", secret="con-test-secret") 1194 | req = oauth.Request.from_consumer_and_token(con, token=tok, 1195 | http_method="GET", http_url=url) 1196 | 1197 | self.assertEqual(req['oauth_token'], tok.key) 1198 | self.assertEqual(req['oauth_consumer_key'], con.key) 1199 | self.assertEqual(tok.verifier, req['oauth_verifier']) 1200 | 1201 | class SignatureMethod_Bad(oauth.SignatureMethod): 1202 | name = "BAD" 1203 | 1204 | def signing_base(self, request, consumer, token): 1205 | return "" 1206 | 1207 | def sign(self, request, consumer, token): 1208 | return "invalid-signature" 1209 | 1210 | 1211 | class TestServer(unittest.TestCase): 1212 | def setUp(self): 1213 | self.url = "http://sp.example.com/" 1214 | 1215 | params = { 1216 | 'oauth_version': "1.0", 1217 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 1218 | 'oauth_timestamp': int(time.time()), 1219 | 'bar': 'blerg', 1220 | 'multi': ['FOO','BAR'], 1221 | 'foo': 59 1222 | } 1223 | 1224 | self.consumer = oauth.Consumer(key="consumer-key", 1225 | secret="consumer-secret") 1226 | self.token = oauth.Token(key="token-key", secret="token-secret") 1227 | 1228 | params['oauth_token'] = self.token.key 1229 | params['oauth_consumer_key'] = self.consumer.key 1230 | self.request = oauth.Request(method="GET", url=self.url, parameters=params) 1231 | 1232 | signature_method = oauth.SignatureMethod_HMAC_SHA1() 1233 | self.request.sign_request(signature_method, self.consumer, self.token) 1234 | 1235 | def test_init(self): 1236 | server = oauth.Server(signature_methods={'HMAC-SHA1' : oauth.SignatureMethod_HMAC_SHA1()}) 1237 | self.assertTrue('HMAC-SHA1' in server.signature_methods) 1238 | self.assertTrue(isinstance(server.signature_methods['HMAC-SHA1'], 1239 | oauth.SignatureMethod_HMAC_SHA1)) 1240 | 1241 | server = oauth.Server() 1242 | self.assertEqual(server.signature_methods, {}) 1243 | 1244 | def test_add_signature_method(self): 1245 | server = oauth.Server() 1246 | res = server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1()) 1247 | self.assertTrue(len(res) == 1) 1248 | self.assertTrue('HMAC-SHA1' in res) 1249 | self.assertTrue(isinstance(res['HMAC-SHA1'], 1250 | oauth.SignatureMethod_HMAC_SHA1)) 1251 | 1252 | res = server.add_signature_method(oauth.SignatureMethod_PLAINTEXT()) 1253 | self.assertTrue(len(res) == 2) 1254 | self.assertTrue('PLAINTEXT' in res) 1255 | self.assertTrue(isinstance(res['PLAINTEXT'], 1256 | oauth.SignatureMethod_PLAINTEXT)) 1257 | 1258 | def test_verify_request(self): 1259 | server = oauth.Server() 1260 | server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1()) 1261 | 1262 | parameters = server.verify_request(self.request, self.consumer, 1263 | self.token) 1264 | 1265 | self.assertTrue('bar' in parameters) 1266 | self.assertTrue('foo' in parameters) 1267 | self.assertTrue('multi' in parameters) 1268 | self.assertEqual(parameters['bar'], 'blerg') 1269 | self.assertEqual(parameters['foo'], 59) 1270 | self.assertEqual(parameters['multi'], ['FOO','BAR']) 1271 | 1272 | def test_verify_request_query_string(self): 1273 | server = oauth.Server() 1274 | server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1()) 1275 | 1276 | signature_method = oauth.SignatureMethod_HMAC_SHA1() 1277 | request2 = oauth.Request.from_request("GET", self.url, query_string=urlencode(dict(self.request))) 1278 | request2.sign_request(signature_method, self.consumer, self.token) 1279 | request3 = oauth.Request.from_request("GET", self.url, query_string=urlencode(dict(request2))) 1280 | 1281 | parameters = server.verify_request(request3, self.consumer, 1282 | self.token) 1283 | 1284 | def test_verify_request_missing_signature(self): 1285 | from oauth2 import MissingSignature 1286 | server = oauth.Server() 1287 | server.add_signature_method(oauth.SignatureMethod_PLAINTEXT()) 1288 | del self.request['oauth_signature_method'] 1289 | del self.request['oauth_signature'] 1290 | 1291 | self.assertRaises(MissingSignature, 1292 | server.verify_request, self.request, self.consumer, self.token) 1293 | 1294 | def test_verify_request_invalid_signature(self): 1295 | server = oauth.Server() 1296 | server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1()) 1297 | self.request['oauth_signature'] = 'BOGUS' 1298 | 1299 | self.assertRaises(oauth.Error, 1300 | server.verify_request, self.request, self.consumer, self.token) 1301 | 1302 | def test_verify_request_invalid_timestamp(self): 1303 | server = oauth.Server() 1304 | server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1()) 1305 | self.request['oauth_timestamp'] -= 86400 1306 | 1307 | self.assertRaises(oauth.Error, 1308 | server.verify_request, self.request, self.consumer, self.token) 1309 | 1310 | def test_build_authenticate_header(self): 1311 | server = oauth.Server() 1312 | headers = server.build_authenticate_header('example.com') 1313 | self.assertTrue('WWW-Authenticate' in headers) 1314 | self.assertEqual('OAuth realm="example.com"', 1315 | headers['WWW-Authenticate']) 1316 | 1317 | def test_no_version(self): 1318 | url = "http://sp.example.com/" 1319 | 1320 | params = { 1321 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 1322 | 'oauth_timestamp': int(time.time()), 1323 | 'bar': 'blerg', 1324 | 'multi': ['FOO','BAR'], 1325 | 'foo': 59 1326 | } 1327 | 1328 | self.consumer = oauth.Consumer(key="consumer-key", 1329 | secret="consumer-secret") 1330 | self.token = oauth.Token(key="token-key", secret="token-secret") 1331 | 1332 | params['oauth_token'] = self.token.key 1333 | params['oauth_consumer_key'] = self.consumer.key 1334 | self.request = oauth.Request(method="GET", url=url, parameters=params) 1335 | 1336 | signature_method = oauth.SignatureMethod_HMAC_SHA1() 1337 | self.request.sign_request(signature_method, self.consumer, self.token) 1338 | 1339 | server = oauth.Server() 1340 | server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1()) 1341 | 1342 | parameters = server.verify_request(self.request, self.consumer, 1343 | self.token) 1344 | 1345 | def test_invalid_version(self): 1346 | url = "http://sp.example.com/" 1347 | 1348 | params = { 1349 | 'oauth_version': '222.9922', 1350 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 1351 | 'oauth_timestamp': int(time.time()), 1352 | 'bar': 'blerg', 1353 | 'multi': ['foo','bar'], 1354 | 'foo': 59 1355 | } 1356 | 1357 | consumer = oauth.Consumer(key="consumer-key", 1358 | secret="consumer-secret") 1359 | token = oauth.Token(key="token-key", secret="token-secret") 1360 | 1361 | params['oauth_token'] = token.key 1362 | params['oauth_consumer_key'] = consumer.key 1363 | request = oauth.Request(method="GET", url=url, parameters=params) 1364 | 1365 | signature_method = oauth.SignatureMethod_HMAC_SHA1() 1366 | request.sign_request(signature_method, consumer, token) 1367 | 1368 | server = oauth.Server() 1369 | server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1()) 1370 | 1371 | self.assertRaises(oauth.Error, server.verify_request, request, consumer, token) 1372 | 1373 | def test_invalid_signature_method(self): 1374 | url = "http://sp.example.com/" 1375 | 1376 | params = { 1377 | 'oauth_version': '1.0', 1378 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 1379 | 'oauth_timestamp': int(time.time()), 1380 | 'bar': 'blerg', 1381 | 'multi': ['FOO','BAR'], 1382 | 'foo': 59 1383 | } 1384 | 1385 | consumer = oauth.Consumer(key="consumer-key", 1386 | secret="consumer-secret") 1387 | token = oauth.Token(key="token-key", secret="token-secret") 1388 | 1389 | params['oauth_token'] = token.key 1390 | params['oauth_consumer_key'] = consumer.key 1391 | request = oauth.Request(method="GET", url=url, parameters=params) 1392 | 1393 | signature_method = SignatureMethod_Bad() 1394 | request.sign_request(signature_method, consumer, token) 1395 | 1396 | server = oauth.Server() 1397 | server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1()) 1398 | 1399 | self.assertRaises(oauth.Error, server.verify_request, request, 1400 | consumer, token) 1401 | 1402 | def test_missing_signature(self): 1403 | url = "http://sp.example.com/" 1404 | 1405 | params = { 1406 | 'oauth_version': '1.0', 1407 | 'oauth_nonce': "4572616e48616d6d65724c61686176", 1408 | 'oauth_timestamp': int(time.time()), 1409 | 'bar': 'blerg', 1410 | 'multi': ['FOO','BAR'], 1411 | 'foo': 59 1412 | } 1413 | 1414 | consumer = oauth.Consumer(key="consumer-key", 1415 | secret="consumer-secret") 1416 | token = oauth.Token(key="token-key", secret="token-secret") 1417 | 1418 | params['oauth_token'] = token.key 1419 | params['oauth_consumer_key'] = consumer.key 1420 | request = oauth.Request(method="GET", url=url, parameters=params) 1421 | 1422 | signature_method = oauth.SignatureMethod_HMAC_SHA1() 1423 | request.sign_request(signature_method, consumer, token) 1424 | del request['oauth_signature'] 1425 | 1426 | server = oauth.Server() 1427 | server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1()) 1428 | 1429 | self.assertRaises(oauth.MissingSignature, server.verify_request, 1430 | request, consumer, token) 1431 | 1432 | 1433 | # Request Token: http://oauth-sandbox.sevengoslings.net/request_token 1434 | # Auth: http://oauth-sandbox.sevengoslings.net/authorize 1435 | # Access Token: http://oauth-sandbox.sevengoslings.net/access_token 1436 | # Two-legged: http://oauth-sandbox.sevengoslings.net/two_legged 1437 | # Three-legged: http://oauth-sandbox.sevengoslings.net/three_legged 1438 | # Key: bd37aed57e15df53 1439 | # Secret: 0e9e6413a9ef49510a4f68ed02cd 1440 | class TestClient(unittest.TestCase): 1441 | # oauth_uris = { 1442 | # 'request_token': '/request_token.php', 1443 | # 'access_token': '/access_token.php' 1444 | # } 1445 | 1446 | oauth_uris = { 1447 | 'request_token': '/request_token', 1448 | 'authorize': '/authorize', 1449 | 'access_token': '/access_token', 1450 | 'two_legged': '/two_legged', 1451 | 'three_legged': '/three_legged' 1452 | } 1453 | 1454 | consumer_key = 'bd37aed57e15df53' 1455 | consumer_secret = '0e9e6413a9ef49510a4f68ed02cd' 1456 | host = 'http://oauth-sandbox.sevengoslings.net' 1457 | 1458 | def setUp(self): 1459 | self.consumer = oauth.Consumer(key=self.consumer_key, 1460 | secret=self.consumer_secret) 1461 | 1462 | self.body = { 1463 | 'foo': 'bar', 1464 | 'bar': 'foo', 1465 | 'multi': ['FOO','BAR'], 1466 | 'blah': 599999 1467 | } 1468 | 1469 | def _uri(self, type): 1470 | uri = self.oauth_uris.get(type) 1471 | if uri is None: 1472 | raise KeyError("%s is not a valid OAuth URI type." % type) 1473 | 1474 | return "%s%s" % (self.host, uri) 1475 | 1476 | def create_simple_multipart_data(self, data): 1477 | boundary = '---Boundary-%d' % random.randint(1,1000) 1478 | crlf = '\r\n' 1479 | items = [] 1480 | for key, value in data.items(): 1481 | items += [ 1482 | '--'+boundary, 1483 | 'Content-Disposition: form-data; name="%s"'%str(key), 1484 | '', 1485 | str(value), 1486 | ] 1487 | items += ['', '--'+boundary+'--', ''] 1488 | content_type = 'multipart/form-data; boundary=%s' % boundary 1489 | return content_type, crlf.join(items).encode('ascii') 1490 | 1491 | def test_init(self): 1492 | class Blah(): 1493 | pass 1494 | 1495 | try: 1496 | client = oauth.Client(Blah()) 1497 | self.fail("Client.__init__() accepted invalid Consumer.") 1498 | except ValueError: 1499 | pass 1500 | 1501 | consumer = oauth.Consumer('token', 'secret') 1502 | try: 1503 | client = oauth.Client(consumer, Blah()) 1504 | self.fail("Client.__init__() accepted invalid Token.") 1505 | except ValueError: 1506 | pass 1507 | 1508 | def test_init_passes_kwargs_to_httplib2(self): 1509 | class Blah(): 1510 | pass 1511 | 1512 | consumer = oauth.Consumer('token', 'secret') 1513 | 1514 | # httplib2 options 1515 | client = oauth.Client(consumer, None, cache='.cache', timeout=3, disable_ssl_certificate_validation=True) 1516 | self.assertNotEqual(client.cache, None) 1517 | self.assertEqual(client.timeout, 3) 1518 | 1519 | 1520 | def test_access_token_get(self): 1521 | """Test getting an access token via GET.""" 1522 | client = oauth.Client(self.consumer, None) 1523 | resp, content = client.request(self._uri('request_token'), "GET") 1524 | 1525 | self.assertEqual(int(resp['status']), 200) 1526 | 1527 | def test_access_token_post(self): 1528 | """Test getting an access token via POST.""" 1529 | client = oauth.Client(self.consumer, None) 1530 | resp, content = client.request(self._uri('request_token'), "POST") 1531 | 1532 | self.assertEqual(int(resp['status']), 200) 1533 | 1534 | res = dict(parse_qsl(content)) 1535 | self.assertTrue(b'oauth_token' in res) 1536 | self.assertTrue(b'oauth_token_secret' in res) 1537 | 1538 | def _two_legged(self, method): 1539 | client = oauth.Client(self.consumer, None) 1540 | 1541 | body = urlencode(self.body).encode('ascii') 1542 | return client.request(self._uri('two_legged'), method, body=body) 1543 | 1544 | def test_two_legged_post(self): 1545 | """A test of a two-legged OAuth POST request.""" 1546 | resp, content = self._two_legged("POST") 1547 | 1548 | self.assertEqual(int(resp['status']), 200) 1549 | 1550 | def test_two_legged_get(self): 1551 | """A test of a two-legged OAuth GET request.""" 1552 | resp, content = self._two_legged("GET") 1553 | self.assertEqual(int(resp['status']), 200) 1554 | 1555 | @mock.patch('httplib2.Http.request') 1556 | def test_multipart_post_does_not_alter_body(self, mockHttpRequest): 1557 | random_result = random.randint(1,100) 1558 | 1559 | data = { 1560 | 'rand-%d'%random.randint(1,100):random.randint(1,100), 1561 | } 1562 | content_type, body = self.create_simple_multipart_data(data) 1563 | 1564 | client = oauth.Client(self.consumer, None) 1565 | uri = self._uri('two_legged') 1566 | 1567 | def mockrequest(cl, ur, **kw): 1568 | self.assertTrue(cl is client) 1569 | self.assertTrue(ur is uri) 1570 | self.assertEqual(frozenset(kw.keys()), frozenset(['method', 'body', 'redirections', 'connection_type', 'headers'])) 1571 | self.assertEqual(kw['body'], body) 1572 | self.assertEqual(kw['connection_type'], None) 1573 | self.assertEqual(kw['method'], 'POST') 1574 | self.assertEqual(kw['redirections'], 1575 | httplib2.DEFAULT_MAX_REDIRECTS) 1576 | self.assertTrue(isinstance(kw['headers'], dict)) 1577 | 1578 | return random_result 1579 | 1580 | mockHttpRequest.side_effect = mockrequest 1581 | 1582 | result = client.request(uri, 'POST', 1583 | headers={'Content-Type':content_type}, 1584 | body=body) 1585 | self.assertEqual(result, random_result) 1586 | 1587 | @mock.patch('httplib2.Http.request') 1588 | def test_url_with_query_string(self, mockHttpRequest): 1589 | uri = 'http://example.com/foo/bar/?show=thundercats&character=snarf' 1590 | client = oauth.Client(self.consumer, None) 1591 | random_result = random.randint(1,100) 1592 | 1593 | def mockrequest(cl, ur, **kw): 1594 | self.assertTrue(cl is client) 1595 | self.assertEqual(frozenset(kw.keys()), 1596 | frozenset(['method', 'body', 'redirections', 1597 | 'connection_type', 'headers'])) 1598 | self.assertEqual(kw['body'], b'') 1599 | self.assertEqual(kw['connection_type'], None) 1600 | self.assertEqual(kw['method'], 'GET') 1601 | self.assertEqual(kw['redirections'], 1602 | httplib2.DEFAULT_MAX_REDIRECTS) 1603 | self.assertTrue(isinstance(kw['headers'], dict)) 1604 | 1605 | req = oauth.Request.from_consumer_and_token(self.consumer, None, 1606 | http_method='GET', http_url=uri, parameters={}) 1607 | req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), 1608 | self.consumer, None) 1609 | expected = parse_qsl( 1610 | urlparse(req.to_url()).query) 1611 | actual = parse_qsl(urlparse(ur).query) 1612 | self.assertEqual(len(expected), len(actual)) 1613 | actual = dict(actual) 1614 | for key, value in expected: 1615 | if key not in ('oauth_signature', 1616 | 'oauth_nonce', 'oauth_timestamp'): 1617 | self.assertEqual(actual[key], value) 1618 | 1619 | return random_result 1620 | 1621 | mockHttpRequest.side_effect = mockrequest 1622 | 1623 | client.request(uri, 'GET') 1624 | 1625 | @mock.patch('httplib2.Http.request') 1626 | @mock.patch('oauth2.Request.from_consumer_and_token') 1627 | def test_multiple_values_for_a_key(self, mockReqConstructor, mockHttpRequest): 1628 | client = oauth.Client(self.consumer, None) 1629 | 1630 | request = oauth.Request("GET", "http://example.com/fetch.php", parameters={'multi': ['1', '2']}) 1631 | mockReqConstructor.return_value = request 1632 | 1633 | client.request('http://whatever', 'POST', body='multi=1&multi=2') 1634 | 1635 | self.assertEqual(mockReqConstructor.call_count, 1) 1636 | self.assertEqual(mockReqConstructor.call_args[1]['parameters'], {'multi': ['1', '2']}) 1637 | 1638 | self.assertTrue('multi=1' in mockHttpRequest.call_args[1]['body']) 1639 | self.assertTrue('multi=2' in mockHttpRequest.call_args[1]['body']) 1640 | 1641 | if __name__ == "__main__": 1642 | import os 1643 | import sys 1644 | sys.path[0:0] = [os.path.join(os.path.dirname(__file__), ".."),] 1645 | unittest.main() 1646 | --------------------------------------------------------------------------------