├── 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 | [](https://gitter.im/joestump/python-oauth2?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://travis-ci.org/joestump/python-oauth2) [](https://codecov.io/gh/joestump/python-oauth2)  
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 |
--------------------------------------------------------------------------------