diff options
author | Jonathan Huot <JonathanHuot@users.noreply.github.com> | 2019-07-04 09:34:36 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-07-04 09:34:36 +0200 |
commit | 4112c2acb4b55b4dff679e83dc645e072e65ca65 (patch) | |
tree | 409c39dd1b0edb7e7e8de7cd487da754aa25a2bc | |
parent | 588abb50010d434c0de5ad9c479d666b7b6ab0bd (diff) | |
parent | d7b90fc841694f126ec63500ea8f74330c4672eb (diff) | |
download | oauthlib-4112c2acb4b55b4dff679e83dc645e072e65ca65.tar.gz |
Merge branch 'master' into oidc-userinfo
-rw-r--r-- | .github/FUNDING.yml | 12 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/backend_application.py | 1 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/endpoints/base.py | 31 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/endpoints/introspect.py | 3 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/endpoints/revocation.py | 3 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/endpoints/token.py | 10 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/parameters.py | 14 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/endpoints/test_base_endpoint.py | 2 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/endpoints/test_error_responses.py | 55 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py | 30 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py | 29 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/test_parameters.py | 27 | ||||
-rw-r--r-- | tests/openid/connect/core/grant_types/test_base.py | 2 |
13 files changed, 206 insertions, 13 deletions
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..9d4faec --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with a single custom sponsorship URL diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py index 2483e56..5737814 100644 --- a/oauthlib/oauth2/rfc6749/clients/backend_application.py +++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py @@ -71,5 +71,6 @@ class BackendApplicationClient(Client): """ kwargs['client_id'] = self.client_id kwargs['include_client_id'] = include_client_id + scope = self.scope if scope is None else scope return prepare_token_request(self.grant_type, body=body, scope=scope, **kwargs) diff --git a/oauthlib/oauth2/rfc6749/endpoints/base.py b/oauthlib/oauth2/rfc6749/endpoints/base.py index c0fc726..e39232f 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/base.py +++ b/oauthlib/oauth2/rfc6749/endpoints/base.py @@ -15,6 +15,8 @@ from ..errors import (FatalClientError, OAuth2Error, ServerError, TemporarilyUnavailableError, InvalidRequestError, InvalidClientError, UnsupportedTokenTypeError) +from oauthlib.common import CaseInsensitiveDict, urldecode + log = logging.getLogger(__name__) @@ -23,6 +25,18 @@ class BaseEndpoint(object): def __init__(self): self._available = True self._catch_errors = False + self._valid_request_methods = None + + @property + def valid_request_methods(self): + return self._valid_request_methods + + @valid_request_methods.setter + def valid_request_methods(self, valid_request_methods): + if valid_request_methods is not None: + valid_request_methods = [x.upper() for x in valid_request_methods] + self._valid_request_methods = valid_request_methods + @property def available(self): @@ -30,7 +44,7 @@ class BaseEndpoint(object): @available.setter def available(self, available): - self._available = available + self._available = available @property def catch_errors(self): @@ -62,6 +76,21 @@ class BaseEndpoint(object): request.token_type_hint not in self.supported_token_types): raise UnsupportedTokenTypeError(request=request) + def _raise_on_bad_method(self, request): + if self.valid_request_methods is None: + raise ValueError('Configure "valid_request_methods" property first') + if request.http_method.upper() not in self.valid_request_methods: + raise InvalidRequestError(request=request, + description=('Unsupported request method %s' % request.http_method.upper())) + + def _raise_on_bad_post_request(self, request): + """Raise if invalid POST request received + """ + if request.http_method.upper() == 'POST': + query_params = request.uri_query or "" + if query_params: + raise InvalidRequestError(request=request, + description=('URL query parameters are not allowed')) def catch_errors_and_unavailability(f): @functools.wraps(f) diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py index 47022fd..4accbdc 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/introspect.py +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -39,6 +39,7 @@ class IntrospectEndpoint(BaseEndpoint): """ valid_token_types = ('access_token', 'refresh_token') + valid_request_methods = ('POST',) def __init__(self, request_validator, supported_token_types=None): BaseEndpoint.__init__(self) @@ -117,6 +118,8 @@ class IntrospectEndpoint(BaseEndpoint): .. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5 .. _`RFC6749`: http://tools.ietf.org/html/rfc6749 """ + self._raise_on_bad_method(request) + self._raise_on_bad_post_request(request) self._raise_on_missing_token(request) self._raise_on_invalid_client(request) self._raise_on_unsupported_token(request) diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py index fda3f30..1fabd03 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py +++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py @@ -28,6 +28,7 @@ class RevocationEndpoint(BaseEndpoint): """ valid_token_types = ('access_token', 'refresh_token') + valid_request_methods = ('POST',) def __init__(self, request_validator, supported_token_types=None, enable_jsonp=False): @@ -121,6 +122,8 @@ class RevocationEndpoint(BaseEndpoint): .. _`Section 4.1.2`: https://tools.ietf.org/html/draft-ietf-oauth-revocation-11#section-4.1.2 .. _`RFC6749`: https://tools.ietf.org/html/rfc6749 """ + self._raise_on_bad_method(request) + self._raise_on_bad_post_request(request) self._raise_on_missing_token(request) self._raise_on_invalid_client(request) self._raise_on_unsupported_token(request) diff --git a/oauthlib/oauth2/rfc6749/endpoints/token.py b/oauthlib/oauth2/rfc6749/endpoints/token.py index 90fb16f..bc87e9b 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/token.py +++ b/oauthlib/oauth2/rfc6749/endpoints/token.py @@ -62,6 +62,8 @@ class TokenEndpoint(BaseEndpoint): .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B """ + valid_request_methods = ('POST',) + def __init__(self, default_grant_type, default_token_type, grant_types): BaseEndpoint.__init__(self) self._grant_types = grant_types @@ -85,13 +87,13 @@ class TokenEndpoint(BaseEndpoint): return self._default_token_type @catch_errors_and_unavailability - def create_token_response(self, uri, http_method='GET', body=None, + def create_token_response(self, uri, http_method='POST', body=None, headers=None, credentials=None, grant_type_for_scope=None, claims=None): """Extract grant_type and route to the designated handler.""" request = Request( uri, http_method=http_method, body=body, headers=headers) - + self.validate_token_request(request) # 'scope' is an allowed Token Request param in both the "Resource Owner Password Credentials Grant" # and "Client Credentials Grant" flows # https://tools.ietf.org/html/rfc6749#section-4.3.2 @@ -115,3 +117,7 @@ class TokenEndpoint(BaseEndpoint): request.grant_type, grant_type_handler) return grant_type_handler.create_token_response( request, self.default_token_type) + + def validate_token_request(self, request): + self._raise_on_bad_method(request) + self._raise_on_bad_post_request(request) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 6b9d630..14d4c0d 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -264,12 +264,15 @@ def parse_authorization_code_response(uri, state=None): query = urlparse.urlparse(uri).query params = dict(urlparse.parse_qsl(query)) - if not 'code' in params: - raise MissingCodeError("Missing code parameter in response.") - if state and params.get('state', None) != state: raise MismatchingStateError() + if 'error' in params: + raise_from_error(params.get('error'), params) + + if not 'code' in params: + raise MissingCodeError("Missing code parameter in response.") + return params @@ -419,7 +422,10 @@ def parse_token_response(body, scope=None): params['scope'] = scope_to_list(params['scope']) if 'expires_in' in params: - params['expires_at'] = time.time() + int(params['expires_in']) + if params['expires_in'] is None: + params.pop('expires_in') + else: + params['expires_at'] = time.time() + int(params['expires_in']) params = OAuth2Token(params, old_scope=scope) validate_token_parameters(params) diff --git a/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py index 4f78d9b..bf04a42 100644 --- a/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py @@ -25,7 +25,7 @@ class BaseEndpointTest(TestCase): server = Server(validator) server.catch_errors = True h, b, s = server.create_token_response( - 'https://example.com?grant_type=authorization_code&code=abc' + 'https://example.com', body='grant_type=authorization_code&code=abc' ) self.assertIn("server_error", b) self.assertEqual(s, 500) diff --git a/tests/oauth2/rfc6749/endpoints/test_error_responses.py b/tests/oauth2/rfc6749/endpoints/test_error_responses.py index a249cb1..2479836 100644 --- a/tests/oauth2/rfc6749/endpoints/test_error_responses.py +++ b/tests/oauth2/rfc6749/endpoints/test_error_responses.py @@ -6,11 +6,11 @@ import json import mock +from oauthlib.common import urlencode from oauthlib.oauth2 import (BackendApplicationServer, LegacyApplicationServer, MobileApplicationServer, RequestValidator, WebApplicationServer) from oauthlib.oauth2.rfc6749 import errors - from ....unittest import TestCase @@ -437,3 +437,56 @@ class ErrorResponseTest(TestCase): _, body, _ = self.backend.create_token_response('https://i.b/token', body='grant_type=bar') self.assertEqual('unsupported_grant_type', json.loads(body)['error']) + + def test_invalid_request_method(self): + test_methods = ['GET', 'pUt', 'dEleTe', 'paTcH'] + test_methods = test_methods + [x.lower() for x in test_methods] + [x.upper() for x in test_methods] + for method in test_methods: + self.validator.authenticate_client.side_effect = self.set_client + + uri = "http://i/b/token/" + try: + _, body, s = self.web.create_token_response(uri, + body='grant_type=access_token&code=123', http_method=method) + self.fail('This should have failed with InvalidRequestError') + except errors.InvalidRequestError as ire: + self.assertIn('Unsupported request method', ire.description) + + try: + _, body, s = self.legacy.create_token_response(uri, + body='grant_type=access_token&code=123', http_method=method) + self.fail('This should have failed with InvalidRequestError') + except errors.InvalidRequestError as ire: + self.assertIn('Unsupported request method', ire.description) + + try: + _, body, s = self.backend.create_token_response(uri, + body='grant_type=access_token&code=123', http_method=method) + self.fail('This should have failed with InvalidRequestError') + except errors.InvalidRequestError as ire: + self.assertIn('Unsupported request method', ire.description) + + def test_invalid_post_request(self): + self.validator.authenticate_client.side_effect = self.set_client + for param in ['token', 'secret', 'code', 'foo']: + uri = 'https://i/b/token?' + urlencode([(param, 'secret')]) + try: + _, body, s = self.web.create_token_response(uri, + body='grant_type=access_token&code=123') + self.fail('This should have failed with InvalidRequestError') + except errors.InvalidRequestError as ire: + self.assertIn('URL query parameters are not allowed', ire.description) + + try: + _, body, s = self.legacy.create_token_response(uri, + body='grant_type=access_token&code=123') + self.fail('This should have failed with InvalidRequestError') + except errors.InvalidRequestError as ire: + self.assertIn('URL query parameters are not allowed', ire.description) + + try: + _, body, s = self.backend.create_token_response(uri, + body='grant_type=access_token&code=123') + self.fail('This should have failed with InvalidRequestError') + except errors.InvalidRequestError as ire: + self.assertIn('URL query parameters are not allowed', ire.description) diff --git a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py index b9bf76a..ae3deae 100644 --- a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py @@ -139,3 +139,33 @@ class IntrospectEndpointTest(TestCase): self.assertEqual(h, self.resp_h) self.assertEqual(loads(b)['error'], 'invalid_request') self.assertEqual(s, 400) + + def test_introspect_invalid_request_method(self): + endpoint = IntrospectEndpoint(self.validator, + supported_token_types=['access_token']) + test_methods = ['GET', 'pUt', 'dEleTe', 'paTcH'] + test_methods = test_methods + [x.lower() for x in test_methods] + [x.upper() for x in test_methods] + for method in test_methods: + body = urlencode([('token', 'foo'), + ('token_type_hint', 'refresh_token')]) + h, b, s = endpoint.create_introspect_response(self.uri, + http_method = method, headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b)['error'], 'invalid_request') + self.assertIn('Unsupported request method', loads(b)['error_description']) + self.assertEqual(s, 400) + + def test_introspect_bad_post_request(self): + endpoint = IntrospectEndpoint(self.validator, + supported_token_types=['access_token']) + for param in ['token', 'secret', 'code', 'foo']: + uri = 'http://some.endpoint?' + urlencode([(param, 'secret')]) + body = urlencode([('token', 'foo'), + ('token_type_hint', 'access_token')]) + h, b, s = endpoint.create_introspect_response( + uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b)['error'], 'invalid_request') + self.assertIn('query parameters are not allowed', loads(b)['error_description']) + self.assertEqual(s, 400) diff --git a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py index 2a24177..17be3a5 100644 --- a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py @@ -120,3 +120,32 @@ class RevocationEndpointTest(TestCase): self.assertEqual(h, self.resp_h) self.assertEqual(loads(b)['error'], 'invalid_request') self.assertEqual(s, 400) + + def test_revoke_invalid_request_method(self): + endpoint = RevocationEndpoint(self.validator, + supported_token_types=['access_token']) + test_methods = ['GET', 'pUt', 'dEleTe', 'paTcH'] + test_methods = test_methods + [x.lower() for x in test_methods] + [x.upper() for x in test_methods] + for method in test_methods: + body = urlencode([('token', 'foo'), + ('token_type_hint', 'refresh_token')]) + h, b, s = endpoint.create_revocation_response(self.uri, + http_method = method, headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b)['error'], 'invalid_request') + self.assertIn('Unsupported request method', loads(b)['error_description']) + self.assertEqual(s, 400) + + def test_revoke_bad_post_request(self): + endpoint = RevocationEndpoint(self.validator, + supported_token_types=['access_token']) + for param in ['token', 'secret', 'code', 'foo']: + uri = 'http://some.endpoint?' + urlencode([(param, 'secret')]) + body = urlencode([('token', 'foo'), + ('token_type_hint', 'access_token')]) + h, b, s = endpoint.create_revocation_response(uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b)['error'], 'invalid_request') + self.assertIn('query parameters are not allowed', loads(b)['error_description']) + self.assertEqual(s, 400) diff --git a/tests/oauth2/rfc6749/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py index c42f516..48b7eac 100644 --- a/tests/oauth2/rfc6749/test_parameters.py +++ b/tests/oauth2/rfc6749/test_parameters.py @@ -73,7 +73,8 @@ class ParameterTests(TestCase): error_nocode = 'https://client.example.com/cb?state=xyz' error_nostate = 'https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA' error_wrongstate = 'https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=abc' - error_response = 'https://client.example.com/cb?error=access_denied&state=xyz' + error_denied = 'https://client.example.com/cb?error=access_denied&state=xyz' + error_invalid = 'https://client.example.com/cb?error=invalid_request&state=xyz' implicit_base = 'https://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&scope=abc&' implicit_response = implicit_base + 'state={0}&token_type=example&expires_in=3600'.format(state) @@ -102,6 +103,15 @@ class ParameterTests(TestCase): ' "expires_in": 3600,' ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",' ' "example_parameter": "example_value" }') + json_response_noexpire = ('{ "access_token": "2YotnFZFEjr1zCsicMWpAA",' + ' "token_type": "example",' + ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",' + ' "example_parameter": "example_value"}') + json_response_expirenull = ('{ "access_token": "2YotnFZFEjr1zCsicMWpAA",' + ' "token_type": "example",' + ' "expires_in": null,' + ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",' + ' "example_parameter": "example_value"}') json_custom_error = '{ "error": "incorrect_client_credentials" }' json_error = '{ "error": "access_denied" }' @@ -135,6 +145,13 @@ class ParameterTests(TestCase): 'example_parameter': 'example_value' } + json_noexpire_dict = { + 'access_token': '2YotnFZFEjr1zCsicMWpAA', + 'token_type': 'example', + 'refresh_token': 'tGzv3JOkF0XG5Qx2TlKWIA', + 'example_parameter': 'example_value' + } + json_notype_dict = { 'access_token': '2YotnFZFEjr1zCsicMWpAA', 'expires_in': 3600, @@ -180,8 +197,10 @@ class ParameterTests(TestCase): self.assertRaises(MissingCodeError, parse_authorization_code_response, self.error_nocode) - self.assertRaises(MissingCodeError, parse_authorization_code_response, - self.error_response) + self.assertRaises(AccessDeniedError, parse_authorization_code_response, + self.error_denied) + self.assertRaises(InvalidRequestFatalError, parse_authorization_code_response, + self.error_invalid) self.assertRaises(MismatchingStateError, parse_authorization_code_response, self.error_nostate, state=self.state) self.assertRaises(MismatchingStateError, parse_authorization_code_response, @@ -209,6 +228,8 @@ class ParameterTests(TestCase): self.assertEqual(parse_token_response(self.json_response_noscope, scope=['all', 'the', 'scopes']), self.json_noscope_dict) + self.assertEqual(parse_token_response(self.json_response_noexpire), self.json_noexpire_dict) + self.assertEqual(parse_token_response(self.json_response_expirenull), self.json_noexpire_dict) scope_changes_recorded = [] def record_scope_change(sender, message, old, new): diff --git a/tests/openid/connect/core/grant_types/test_base.py b/tests/openid/connect/core/grant_types/test_base.py index 76e017f..d506b7e 100644 --- a/tests/openid/connect/core/grant_types/test_base.py +++ b/tests/openid/connect/core/grant_types/test_base.py @@ -68,7 +68,7 @@ class IDTokenTest(TestCase): self.assertEqual(token["id_token"], "eyJ.body.signature") id_token = self.mock_validator.finalize_id_token.call_args[0][0] self.assertEqual(id_token['aud'], 'abcdef') - self.assertGreaterEqual(id_token['iat'], int(time.time())) + self.assertGreaterEqual(int(time.time()), id_token['iat']) def test_finalize_id_token_with_nonce(self): token = self.grant.add_id_token(self.token, "token_handler_mock", self.request, "my_nonce") |