diff options
-rw-r--r-- | .travis.yml | 2 | ||||
-rw-r--r-- | AUTHORS | 1 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/backend_application.py | 26 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/legacy_application.py | 26 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/mobile_application.py | 18 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/web_application.py | 26 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/parameters.py | 15 | ||||
-rw-r--r-- | oauthlib/signals.py | 41 | ||||
-rwxr-xr-x | setup.py | 12 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/clients/test_backend_application.py | 16 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/clients/test_legacy_application.py | 16 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/clients/test_mobile_application.py | 16 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/clients/test_web_application.py | 17 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/test_parameters.py | 33 |
14 files changed, 199 insertions, 66 deletions
diff --git a/.travis.yml b/.travis.yml index 8d9c572..f8c39b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: install: - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install --use-mirrors unittest2; fi - - pip install nose pycrypto mock pyjwt + - pip install nose pycrypto mock pyjwt blinker script: - nosetests -w tests @@ -21,3 +21,4 @@ Kyle Valade Tyler Jones (squirly) Massimiliano Pippi Josh Turmel +David Baumgold diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py index 9e0d438..103fe6f 100644 --- a/oauthlib/oauth2/rfc6749/clients/backend_application.py +++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py @@ -72,7 +72,7 @@ class BackendApplicationClient(Client): :param body: The response body from the token request. :param scope: Scopes originally requested. :return: Dictionary of token parameters. - :raises: Warning if scope has changed. OAuth2Error if response is invalid. + :raises: OAuth2Error if response is invalid. These response are json encoded and could easily be parsed without the assistance of OAuthLib. However, there are a few subtle issues @@ -123,18 +123,19 @@ class BackendApplicationClient(Client): 'scope': ['hello', 'world'], # note the list } - If there was a scope change you will be notified with a warning:: - + If there was a scope change, a "scope changed" signal will be dispatched + using the `blinker`_ library, if installed. (If blinker is not installed, + there will be no notification on scope change.) To be automatically + notified when the returned scope is different from the requested scope, + simply connect a function to the ``oauthlib.signals.scope_changed`` + dispatcher:: + + >>> def alert_scope_changed(message, old, new): + ... print(message, old, new) + ... + >>> oauthlib.signals.scope_changed.connect(alert_scope_changed) >>> client.parse_request_body_response(response_body, scope=['images']) - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response - .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 - File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response - validate_token_parameters(params, scope) - File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters - raise Warning("Scope has changed to %s." % new_scope) - Warning: Scope has changed to [u'hello', u'world']. + ('Scope has changed from "images" to "hello world".', ['images'], ['hello', 'world']) If there was an error on the providers side you will be notified with an error. For example, if there was no ``token_type`` provided:: @@ -153,6 +154,7 @@ class BackendApplicationClient(Client): .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 + .. _`blinker`: http://pythonhosted.org/blinker/ """ self.token = parse_token_response(body, scope=scope) self._populate_attributes(self.token) diff --git a/oauthlib/oauth2/rfc6749/clients/legacy_application.py b/oauthlib/oauth2/rfc6749/clients/legacy_application.py index edb68d7..2632d06 100644 --- a/oauthlib/oauth2/rfc6749/clients/legacy_application.py +++ b/oauthlib/oauth2/rfc6749/clients/legacy_application.py @@ -84,7 +84,7 @@ class LegacyApplicationClient(Client): :param body: The response body from the token request. :param scope: Scopes originally requested. :return: Dictionary of token parameters. - :raises: Warning if scope has changed. OAuth2Error if response is invalid. + :raises: OAuth2Error if response is invalid. These response are json encoded and could easily be parsed without the assistance of OAuthLib. However, there are a few subtle issues @@ -135,18 +135,19 @@ class LegacyApplicationClient(Client): 'scope': ['hello', 'world'], # note the list } - If there was a scope change you will be notified with a warning:: - + If there was a scope change, a "scope changed" signal will be dispatched + using the `blinker`_ library, if installed. (If blinker is not installed, + there will be no notification on scope change.) To be automatically + notified when the returned scope is different from the requested scope, + simply connect a function to the ``oauthlib.signals.scope_changed`` + dispatcher:: + + >>> def alert_scope_changed(message, old, new): + ... print(message, old, new) + ... + >>> oauthlib.signals.scope_changed.connect(alert_scope_changed) >>> client.parse_request_body_response(response_body, scope=['images']) - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response - .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 - File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response - validate_token_parameters(params, scope) - File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters - raise Warning("Scope has changed to %s." % new_scope) - Warning: Scope has changed to [u'hello', u'world']. + ('Scope has changed from "images" to "hello world".', ['images'], ['hello', 'world']) If there was an error on the providers side you will be notified with an error. For example, if there was no ``token_type`` provided:: @@ -165,6 +166,7 @@ class LegacyApplicationClient(Client): .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 + .. _`blinker`: http://pythonhosted.org/blinker/ """ self.token = parse_token_response(body, scope=scope) self._populate_attributes(self.token) diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py index 6b79c81..9e3ef21 100644 --- a/oauthlib/oauth2/rfc6749/clients/mobile_application.py +++ b/oauthlib/oauth2/rfc6749/clients/mobile_application.py @@ -108,7 +108,7 @@ class MobileApplicationClient(Client): :param state: The state provided in the authorization request. :param scope: The scopes provided in the authorization request. :return: Dictionary of token parameters. - :raises: Warning if scope has changed. OAuth2Error if response is invalid. + :raises: OAuth2Error if response is invalid. A successful response should always contain @@ -158,16 +158,12 @@ class MobileApplicationClient(Client): File "oauthlib/oauth2/rfc6749/parameters.py", line 197, in parse_implicit_response raise ValueError("Mismatching or missing state in params.") ValueError: Mismatching or missing state in params. - >>> client.parse_request_uri_response(response_uri, scope=['other']) - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - File "oauthlib/oauth2/rfc6749/__init__.py", line 598, in parse_request_uri_response - **scope** - File "oauthlib/oauth2/rfc6749/parameters.py", line 199, in parse_implicit_response - validate_token_parameters(params, scope) - File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters - raise Warning("Scope has changed to %s." % new_scope) - Warning: Scope has changed to [u'hello', u'world']. + >>> def alert_scope_changed(message, old, new): + ... print(message, old, new) + ... + >>> oauthlib.signals.scope_changed.connect(alert_scope_changed) + >>> client.parse_request_body_response(response_body, scope=['other']) + ('Scope has changed from "other" to "hello world".', ['other'], ['hello', 'world']) .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index 6aeb831..941b0e9 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -187,7 +187,7 @@ class WebApplicationClient(Client): :param body: The response body from the token request. :param scope: Scopes originally requested. :return: Dictionary of token parameters. - :raises: Warning if scope has changed. OAuth2Error if response is invalid. + :raises: OAuth2Error if response is invalid. These response are json encoded and could easily be parsed without the assistance of OAuthLib. However, there are a few subtle issues @@ -238,18 +238,19 @@ class WebApplicationClient(Client): 'scope': ['hello', 'world'], # note the list } - If there was a scope change you will be notified with a warning:: - + If there was a scope change, a "scope changed" signal will be dispatched + using the `blinker`_ library, if installed. (If blinker is not installed, + there will be no notification on scope change.) To be automatically + notified when the returned scope is different from the requested scope, + simply connect a function to the ``oauthlib.signals.scope_changed`` + dispatcher:: + + >>> def alert_scope_changed(message, old, new): + ... print(message, old, new) + ... + >>> oauthlib.signals.scope_changed.connect(alert_scope_changed) >>> client.parse_request_body_response(response_body, scope=['images']) - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response - .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 - File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response - validate_token_parameters(params, scope) - File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters - raise Warning("Scope has changed to %s." % new_scope) - Warning: Scope has changed to [u'hello', u'world']. + ('Scope has changed from "images" to "hello world".', ['images'], ['hello', 'world']) If there was an error on the providers side you will be notified with an error. For example, if there was no ``token_type`` provided:: @@ -268,6 +269,7 @@ class WebApplicationClient(Client): .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 + .. _`blinker`: http://pythonhosted.org/blinker/ """ self.token = parse_token_response(body, scope=scope) self._populate_attributes(self.token) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 50269ad..831f84d 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -17,6 +17,7 @@ try: except ImportError: import urllib.parse as urlparse from oauthlib.common import add_params_to_uri, add_params_to_qs, unicode_type +from oauthlib.signals import scope_changed from .errors import raise_from_error, MissingTokenError, MissingTokenTypeError from .errors import MismatchingStateError, MissingCodeError from .errors import InsecureTransportError @@ -390,11 +391,9 @@ def validate_token_parameters(params, scope=None): # parameter to inform the client of the actual scope granted. # http://tools.ietf.org/html/rfc6749#section-3.3 new_scope = params.get('scope', None) - scope = scope_to_list(scope) - if scope and new_scope and set(scope) != set(new_scope): - w = Warning("Scope has changed from {old} to {new}.".format( - old=scope, new=new_scope, - )) - w.old_scope = scope - w.new_scope = new_scope - raise w + old_scope = scope_to_list(scope) + if old_scope and new_scope and set(old_scope) != set(new_scope): + message = 'Scope has changed from "{old}" to "{new}".'.format( + old=scope, new=list_to_scope(new_scope), + ) + scope_changed.send(message=message, old=old_scope, new=new_scope) diff --git a/oauthlib/signals.py b/oauthlib/signals.py new file mode 100644 index 0000000..2f86650 --- /dev/null +++ b/oauthlib/signals.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" + Implements signals based on blinker if available, otherwise + falls silently back to a noop. Shamelessly stolen from flask.signals: + https://github.com/mitsuhiko/flask/blob/master/flask/signals.py +""" +signals_available = False +try: + from blinker import Namespace + signals_available = True +except ImportError: + class Namespace(object): + def signal(self, name, doc=None): + return _FakeSignal(name, doc) + + class _FakeSignal(object): + """If blinker is unavailable, create a fake class with the same + interface that allows sending of signals but will fail with an + error on anything else. Instead of doing anything on send, it + will just ignore the arguments and do nothing instead. + """ + + def __init__(self, name, doc=None): + self.name = name + self.__doc__ = doc + def _fail(self, *args, **kwargs): + raise RuntimeError('signalling support is unavailable ' + 'because the blinker library is ' + 'not installed.') + send = lambda *a, **kw: None + connect = disconnect = has_receivers_for = receivers_for = \ + temporarily_connected_to = connected_to = _fail + del _fail + +# The namespace for code signals. If you are not oauthlib code, do +# not put signals in here. Create your own namespace instead. +_signals = Namespace() + + +# Core signals. +scope_changed = _signals.signal('scope-changed') @@ -18,11 +18,12 @@ def fread(fn): return f.read() if sys.version_info[0] == 3: - tests_require = ['nose', 'pycrypto', 'pyjwt'] + tests_require = ['nose', 'pycrypto', 'pyjwt', 'blinker'] else: - tests_require = ['nose', 'unittest2', 'pycrypto', 'mock', 'pyjwt'] + tests_require = ['nose', 'unittest2', 'pycrypto', 'mock', 'pyjwt', 'blinker'] rsa_require = ['pycrypto'] signedtoken_require = ['pycrypto', 'pyjwt'] +signals_require = ['blinker'] requires = [] @@ -41,7 +42,12 @@ setup( packages=find_packages(exclude=('docs', 'tests', 'tests.*')), test_suite='nose.collector', tests_require=tests_require, - extras_require={'test': tests_require, 'rsa': rsa_require, 'signedtoken': signedtoken_require}, + extras_require={ + 'test': tests_require, + 'rsa': rsa_require, + 'signedtoken': signedtoken_require, + 'signals': signals_require, + }, install_requires=requires, classifiers=[ 'Development Status :: 4 - Beta', diff --git a/tests/oauth2/rfc6749/clients/test_backend_application.py b/tests/oauth2/rfc6749/clients/test_backend_application.py index 19ff1ef..ad6f9f2 100644 --- a/tests/oauth2/rfc6749/clients/test_backend_application.py +++ b/tests/oauth2/rfc6749/clients/test_backend_application.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, unicode_literals from mock import patch from oauthlib.oauth2 import BackendApplicationClient +from oauthlib import signals from ....unittest import TestCase @@ -63,4 +64,17 @@ class BackendApplicationClientTest(TestCase): self.assertEqual(client.token_type, response.get("token_type")) # Mismatching state - self.assertRaises(Warning, client.parse_request_body_response, self.token_json, scope="invalid") + scope_changes_recorded = [] + def record_scope_change(sender, message, old, new): + scope_changes_recorded.append((message, old, new)) + + signals.scope_changed.connect(record_scope_change) + try: + client.parse_request_body_response(self.token_json, scope="invalid") + self.assertEqual(len(scope_changes_recorded), 1) + message, old, new = scope_changes_recorded[0] + self.assertEqual(message, 'Scope has changed from "invalid" to "/profile".') + self.assertEqual(old, ['invalid']) + self.assertEqual(new, ['/profile']) + finally: + signals.scope_changed.disconnect(record_scope_change) diff --git a/tests/oauth2/rfc6749/clients/test_legacy_application.py b/tests/oauth2/rfc6749/clients/test_legacy_application.py index 2af2e6c..a0ed642 100644 --- a/tests/oauth2/rfc6749/clients/test_legacy_application.py +++ b/tests/oauth2/rfc6749/clients/test_legacy_application.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals from mock import patch +from oauthlib import signals from oauthlib.oauth2 import LegacyApplicationClient from ....unittest import TestCase @@ -65,4 +66,17 @@ class LegacyApplicationClientTest(TestCase): self.assertEqual(client.token_type, response.get("token_type")) # Mismatching state - self.assertRaises(Warning, client.parse_request_body_response, self.token_json, scope="invalid") + scope_changes_recorded = [] + def record_scope_change(sender, message, old, new): + scope_changes_recorded.append((message, old, new)) + + signals.scope_changed.connect(record_scope_change) + try: + client.parse_request_body_response(self.token_json, scope="invalid") + self.assertEqual(len(scope_changes_recorded), 1) + message, old, new = scope_changes_recorded[0] + self.assertEqual(message, 'Scope has changed from "invalid" to "/profile".') + self.assertEqual(old, ['invalid']) + self.assertEqual(new, ['/profile']) + finally: + signals.scope_changed.disconnect(record_scope_change) diff --git a/tests/oauth2/rfc6749/clients/test_mobile_application.py b/tests/oauth2/rfc6749/clients/test_mobile_application.py index 7c57101..1186946 100644 --- a/tests/oauth2/rfc6749/clients/test_mobile_application.py +++ b/tests/oauth2/rfc6749/clients/test_mobile_application.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals from mock import patch +from oauthlib import signals from oauthlib.oauth2 import MobileApplicationClient from ....unittest import TestCase @@ -77,4 +78,17 @@ class MobileApplicationClientTest(TestCase): self.assertEqual(client.token_type, response.get("token_type")) # Mismatching scope - self.assertRaises(Warning, client.parse_request_uri_response, self.response_uri, scope="invalid") + scope_changes_recorded = [] + def record_scope_change(sender, message, old, new): + scope_changes_recorded.append((message, old, new)) + + signals.scope_changed.connect(record_scope_change) + try: + client.parse_request_uri_response(self.response_uri, scope="invalid") + self.assertEqual(len(scope_changes_recorded), 1) + message, old, new = scope_changes_recorded[0] + self.assertEqual(message, 'Scope has changed from "invalid" to "/profile".') + self.assertEqual(old, ['invalid']) + self.assertEqual(new, ['/profile']) + finally: + signals.scope_changed.disconnect(record_scope_change) diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 0eac008..6f7b7e1 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -5,7 +5,7 @@ import datetime from mock import patch -from oauthlib import common +from oauthlib import common, signals from oauthlib.oauth2.rfc6749 import utils, errors from oauthlib.oauth2 import Client from oauthlib.oauth2 import WebApplicationClient @@ -128,4 +128,17 @@ class WebApplicationClientTest(TestCase): self.assertEqual(client.token_type, response.get("token_type")) # Mismatching state - self.assertRaises(Warning, client.parse_request_body_response, self.token_json, scope="invalid") + scope_changes_recorded = [] + def record_scope_change(sender, message, old, new): + scope_changes_recorded.append((message, old, new)) + + signals.scope_changed.connect(record_scope_change) + try: + client.parse_request_body_response(self.token_json, scope="invalid") + self.assertEqual(len(scope_changes_recorded), 1) + message, old, new = scope_changes_recorded[0] + self.assertEqual(message, 'Scope has changed from "invalid" to "/profile".') + self.assertEqual(old, ['invalid']) + self.assertEqual(new, ['/profile']) + finally: + signals.scope_changed.disconnect(record_scope_change) diff --git a/tests/oauth2/rfc6749/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py index c411fee..5122293 100644 --- a/tests/oauth2/rfc6749/test_parameters.py +++ b/tests/oauth2/rfc6749/test_parameters.py @@ -5,6 +5,7 @@ from mock import patch from ...unittest import TestCase from oauthlib.oauth2.rfc6749.parameters import * from oauthlib.oauth2.rfc6749.errors import * +from oauthlib import signals @patch('time.time', new=lambda: 1000) @@ -193,7 +194,21 @@ class ParameterTests(TestCase): self.assertEqual(parse_token_response(self.json_response), self.json_dict) self.assertRaises(InvalidRequestError, parse_token_response, self.json_error) self.assertRaises(MissingTokenError, parse_token_response, self.json_notoken) - self.assertRaises(Warning, parse_token_response, self.json_response, scope='aaa') + + scope_changes_recorded = [] + def record_scope_change(sender, message, old, new): + scope_changes_recorded.append((message, old, new)) + + signals.scope_changed.connect(record_scope_change) + try: + parse_token_response(self.json_response, scope='aaa') + self.assertEqual(len(scope_changes_recorded), 1) + message, old, new = scope_changes_recorded[0] + self.assertEqual(message, 'Scope has changed from "aaa" to "abc def".') + self.assertEqual(old, ['aaa']) + self.assertEqual(new, ['abc', 'def']) + finally: + signals.scope_changed.disconnect(record_scope_change) def test_json_token_notype(self): """Verify strict token type parsing only when configured. """ @@ -209,7 +224,21 @@ class ParameterTests(TestCase): self.assertEqual(parse_token_response(self.url_encoded_response), self.json_dict) self.assertRaises(InvalidRequestError, parse_token_response, self.url_encoded_error) self.assertRaises(MissingTokenError, parse_token_response, self.url_encoded_notoken) - self.assertRaises(Warning, parse_token_response, self.url_encoded_response, scope='aaa') + + scope_changes_recorded = [] + def record_scope_change(sender, message, old, new): + scope_changes_recorded.append((message, old, new)) + + signals.scope_changed.connect(record_scope_change) + try: + parse_token_response(self.url_encoded_response, scope='aaa') + self.assertEqual(len(scope_changes_recorded), 1) + message, old, new = scope_changes_recorded[0] + self.assertEqual(message, 'Scope has changed from "aaa" to "abc def".') + self.assertEqual(old, ['aaa']) + self.assertEqual(new, ['abc', 'def']) + finally: + signals.scope_changed.disconnect(record_scope_change) def test_token_response_with_expires(self): """Verify fallback for alternate spelling of expires_in. """ |