From 17593fdeb4f3528a146da9a434e18fcc9f973e1d Mon Sep 17 00:00:00 2001 From: Pierre CHAISY Date: Wed, 26 Apr 2017 17:20:35 +0200 Subject: [PATCH 001/237] remove check on empty scopes --- oauthlib/oauth2/rfc6749/grant_types/implicit.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py index 51e95af6..7ffed8d8 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py +++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py @@ -201,11 +201,6 @@ def create_token_response(self, request, token_handler): .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 """ try: - # request.scopes is only mandated in post auth and both pre and - # post auth use validate_authorization_request - if not request.scopes: - raise ValueError('Scopes must be set on post auth.') - self.validate_token_request(request) # If the request fails due to a missing, invalid, or mismatching From e575cca3e5d18b1e7051c64f435f2cdea71a29ab Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Sun, 1 Oct 2017 03:07:11 -0300 Subject: [PATCH 002/237] OpenID connect improvements (#484) * Change create_token_response to only save access_token when it's present in request.response_type * Remove unused import, fix indentation and improve comment * Fix AuthorizationEndpoint response_type for OpenID Connect hybrid flow * Add new ImplicitTokenGrantDispatcher Changes AuthorizationEndpoint response_type `'token'`, `'id_token'` and `'id_token token'` to work with OpenID Connect and OAuth2 implicit flow in a transparent way * Add new AuthTokenGrantDispatcher Change AuthorizationEndpoint grant_types `'authorization_code'` to work with OpenID Connect and OAuth2 authorization flow in a transparent way * Change tests to include required client_id and redirect_uri * Remove AuthorizationEndpoint grant_types `'openid'` Now OpenID Connect and OAuth2 authorization flow can use `authorization_code` in a transparent way * Add sone blank lines and fix indentation * Change AuthorizationEndpoint grant type id_token and id_token token to use openid_connect_implicit direct * Change default empty value to None and fix a typo * Add assert called to AuthTokenGrantDispatcher tests * Add request to get_authorization_code_scopes --- .../rfc6749/endpoints/pre_configured.py | 23 ++-- .../oauth2/rfc6749/grant_types/__init__.py | 2 + .../oauth2/rfc6749/grant_types/implicit.py | 32 ++--- .../rfc6749/grant_types/openid_connect.py | 65 +++++++++- oauthlib/oauth2/rfc6749/request_validator.py | 24 ++++ .../rfc6749/endpoints/test_claims_handling.py | 2 +- .../rfc6749/endpoints/test_scope_handling.py | 2 +- .../grant_types/test_openid_connect.py | 114 +++++++++++++++++- tests/oauth2/rfc6749/test_server.py | 8 +- 9 files changed, 241 insertions(+), 31 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index 6428b8d1..07c37158 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -9,8 +9,11 @@ from __future__ import absolute_import, unicode_literals from ..grant_types import (AuthCodeGrantDispatcher, AuthorizationCodeGrant, - ClientCredentialsGrant, ImplicitGrant, + AuthTokenGrantDispatcher, + ClientCredentialsGrant, + ImplicitTokenGrantDispatcher, ImplicitGrant, OpenIDConnectAuthCode, OpenIDConnectImplicit, + OpenIDConnectHybrid, RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant) from ..tokens import BearerToken @@ -49,33 +52,37 @@ def __init__(self, request_validator, token_expires_in=None, refresh_grant = RefreshTokenGrant(request_validator) openid_connect_auth = OpenIDConnectAuthCode(request_validator) openid_connect_implicit = OpenIDConnectImplicit(request_validator) + openid_connect_hybrid = OpenIDConnectHybrid(request_validator) bearer = BearerToken(request_validator, token_generator, token_expires_in, refresh_token_generator) - auth_grant_choice = AuthCodeGrantDispatcher( default_auth_grant=auth_grant, oidc_auth_grant=openid_connect_auth) + auth_grant_choice = AuthCodeGrantDispatcher(default_auth_grant=auth_grant, oidc_auth_grant=openid_connect_auth) + implicit_grant_choice = ImplicitTokenGrantDispatcher(default_implicit_grant=implicit_grant, oidc_implicit_grant=openid_connect_implicit) # See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations # internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination AuthorizationEndpoint.__init__(self, default_response_type='code', response_types={ 'code': auth_grant_choice, - 'token': implicit_grant, + 'token': implicit_grant_choice, 'id_token': openid_connect_implicit, 'id_token token': openid_connect_implicit, - 'code token': openid_connect_auth, - 'code id_token': openid_connect_auth, - 'code token id_token': openid_connect_auth, + 'code token': openid_connect_hybrid, + 'code id_token': openid_connect_hybrid, + 'code id_token token': openid_connect_hybrid, 'none': auth_grant }, default_token_type=bearer) + + token_grant_choice = AuthTokenGrantDispatcher(request_validator, default_token_grant=auth_grant, oidc_token_grant=openid_connect_auth) + TokenEndpoint.__init__(self, default_grant_type='authorization_code', grant_types={ - 'authorization_code': auth_grant, + 'authorization_code': token_grant_choice, 'password': password_grant, 'client_credentials': credentials_grant, 'refresh_token': refresh_grant, - 'openid': openid_connect_auth }, default_token_type=bearer) ResourceEndpoint.__init__(self, default_token='Bearer', diff --git a/oauthlib/oauth2/rfc6749/grant_types/__init__.py b/oauthlib/oauth2/rfc6749/grant_types/__init__.py index 1da1281f..2e4bfe4a 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/__init__.py +++ b/oauthlib/oauth2/rfc6749/grant_types/__init__.py @@ -16,3 +16,5 @@ from .openid_connect import OpenIDConnectHybrid from .openid_connect import OIDCNoPrompt from .openid_connect import AuthCodeGrantDispatcher +from .openid_connect import AuthTokenGrantDispatcher +from .openid_connect import ImplicitTokenGrantDispatcher diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py index 858ef774..2b9c49d4 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py +++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py @@ -11,7 +11,6 @@ from oauthlib.uri_validate import is_absolute_uri from .. import errors -from ..request_validator import RequestValidator from .base import GrantTypeBase log = logging.getLogger(__name__) @@ -229,7 +228,7 @@ def create_token_response(self, request, token_handler): return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples, fragment=True)}, None, 302 - # In OIDC implicit flow it is possible to have a request_type that does not include the access token! + # In OIDC implicit flow it is possible to have a request_type that does not include the access_token! # "id_token token" - return the access token and the id token # "id_token" - don't return the access token if "token" in request.response_type.split(): @@ -239,7 +238,12 @@ def create_token_response(self, request, token_handler): for modifier in self._token_modifiers: token = modifier(token, token_handler, request) - self.request_validator.save_token(token, request) + + # In OIDC implicit flow it is possible to have a request_type that does + # not include the access_token! In this case there is no need to save a token. + if "token" in request.response_type.split(): + self.request_validator.save_token(token, request) + return self.prepare_authorization_response( request, token, {}, None, 302) @@ -317,8 +321,7 @@ def validate_token_request(self, request): # Then check for normal errors. request_info = self._run_custom_validators(request, - self.custom_validators.all_pre) - + self.custom_validators.all_pre) # If the resource owner denies the access request or if the request # fails for reasons other than a missing or invalid redirection URI, @@ -352,20 +355,21 @@ def validate_token_request(self, request): self.validate_scopes(request) request_info.update({ - 'client_id': request.client_id, - 'redirect_uri': request.redirect_uri, - 'response_type': request.response_type, - 'state': request.state, - 'request': request, + 'client_id': request.client_id, + 'redirect_uri': request.redirect_uri, + 'response_type': request.response_type, + 'state': request.state, + 'request': request, }) - request_info = self._run_custom_validators(request, - self.custom_validators.all_post, - request_info) + request_info = self._run_custom_validators( + request, + self.custom_validators.all_post, + request_info + ) return request.scopes, request_info - def _run_custom_validators(self, request, validations, diff --git a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py b/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py index 4c988640..4371b286 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py +++ b/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py @@ -12,11 +12,11 @@ from ..errors import ConsentRequired, InvalidRequestError, LoginRequired from ..request_validator import RequestValidator from .authorization_code import AuthorizationCodeGrant -from .base import GrantTypeBase from .implicit import ImplicitGrant log = logging.getLogger(__name__) + class OIDCNoPrompt(Exception): """Exception used to inform users that no explicit authorization is needed. @@ -76,6 +76,65 @@ def validate_authorization_request(self, request): return self._handler_for_request(request).validate_authorization_request(request) +class ImplicitTokenGrantDispatcher(object): + """ + This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope + including 'openid' to either the default_auth_grant or the oidc_auth_grant based on the scopes requested. + """ + def __init__(self, default_implicit_grant=None, oidc_implicit_grant=None): + self.default_implicit_grant = default_implicit_grant + self.oidc_implicit_grant = oidc_implicit_grant + + def _handler_for_request(self, request): + handler = self.default_implicit_grant + + if request.scopes and "openid" in request.scopes and 'id_token' in request.response_type: + handler = self.oidc_implicit_grant + + log.debug('Selecting handler for request %r.', handler) + return handler + + def create_authorization_response(self, request, token_handler): + return self._handler_for_request(request).create_authorization_response(request, token_handler) + + def validate_authorization_request(self, request): + return self._handler_for_request(request).validate_authorization_request(request) + + +class AuthTokenGrantDispatcher(object): + """ + This is an adapter class that will route simple Token requests, those that authorization_code have a scope + including 'openid' to either the default_token_grant or the oidc_token_grant based on the scopes requested. + """ + def __init__(self, request_validator, default_token_grant=None, oidc_token_grant=None): + self.default_token_grant = default_token_grant + self.oidc_token_grant = oidc_token_grant + self.request_validator = request_validator + + def _handler_for_request(self, request): + handler = self.default_token_grant + scopes = () + parameters = dict(request.decoded_body) + client_id = parameters.get('client_id', None) + code = parameters.get('code', None) + redirect_uri = parameters.get('redirect_uri', None) + + # If code is not pressent fallback to `default_token_grant` wich will + # raise an error for the missing `code` in `create_token_response` step. + if code: + scopes = self.request_validator.get_authorization_code_scopes(client_id, code, redirect_uri, request) + + if 'openid' in scopes: + handler = self.oidc_token_grant + + log.debug('Selecting handler for request %r.', handler) + return handler + + def create_token_response(self, request, token_handler): + handler = self._handler_for_request(request) + return handler.create_token_response(request, token_handler) + + class OpenIDConnectBase(object): # Just proxy the majority of method calls through to the @@ -307,7 +366,7 @@ def openid_authorization_validator(self, request): self._inflate_claims(request) if not self.request_validator.validate_user_match( - request.id_token_hint, request.scopes, request.claims, request): + request.id_token_hint, request.scopes, request.claims, request): msg = "Session user does not match client supplied user." raise LoginRequired(request=request, description=msg) @@ -356,6 +415,7 @@ def __init__(self, request_validator=None, **kwargs): self.openid_authorization_validator) self.register_token_modifier(self.add_id_token) + class OpenIDConnectImplicit(OpenIDConnectBase): def __init__(self, request_validator=None, **kwargs): @@ -369,6 +429,7 @@ def __init__(self, request_validator=None, **kwargs): self.openid_implicit_authorization_validator) self.register_token_modifier(self.add_id_token) + class OpenIDConnectHybrid(OpenIDConnectBase): def __init__(self, request_validator=None, **kwargs): diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 0adfa1bd..ba129d5e 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -238,6 +238,30 @@ def save_authorization_code(self, client_id, code, request, *args, **kwargs): """ raise NotImplementedError('Subclasses must implement this method.') + def get_authorization_code_scopes(self, client_id, code, redirect_uri, request): + """ Extracts scopes from saved authorization code. + + The scopes returned by this method is used to route token requests + based on scopes passed to Authorization Code requests. + + With that the token endpoint knows when to include OpenIDConnect + id_token in token response only based on authorization code scopes. + + Only code param should be sufficient to retrieve grant code from + any storage you are using, `client_id` and `redirect_uri` can gave a + blank value `""` don't forget to check it before using those values + in a select query if a database is used. + + :param client_id: Unicode client identifier + :param code: Unicode authorization code grant + :param redirect_uri: Unicode absolute URI + :return: A list of scope + + Method is used by: + - Authorization Token Grant Dispatcher + """ + raise NotImplementedError('Subclasses must implement this method.') + def save_token(self, token, request, *args, **kwargs): """Persist the token with a token type specific method. diff --git a/tests/oauth2/rfc6749/endpoints/test_claims_handling.py b/tests/oauth2/rfc6749/endpoints/test_claims_handling.py index 9795c804..ff72673f 100644 --- a/tests/oauth2/rfc6749/endpoints/test_claims_handling.py +++ b/tests/oauth2/rfc6749/endpoints/test_claims_handling.py @@ -91,7 +91,7 @@ def test_claims_stored_on_code_creation(self): code = get_query_credentials(h['Location'])['code'][0] token_uri = 'http://example.com/path' _, body, _ = self.server.create_token_response(token_uri, - body='grant_type=authorization_code&code=%s' % code) + body='client_id=me&redirect_uri=http://back.to/me&grant_type=authorization_code&code=%s' % code) self.assertDictEqual(self.claims_saved_with_bearer_token, claims) diff --git a/tests/oauth2/rfc6749/endpoints/test_scope_handling.py b/tests/oauth2/rfc6749/endpoints/test_scope_handling.py index 87781b32..8490c03f 100644 --- a/tests/oauth2/rfc6749/endpoints/test_scope_handling.py +++ b/tests/oauth2/rfc6749/endpoints/test_scope_handling.py @@ -87,7 +87,7 @@ def test_scope_preservation(self): self.assertIn('Location', h) code = get_query_credentials(h['Location'])['code'][0] _, body, _ = getattr(self, backend_server_type).create_token_response(token_uri, - body='grant_type=authorization_code&code=%s' % code) + body='client_id=me&redirect_uri=http://back.to/me&grant_type=authorization_code&code=%s' % code) self.assertEqual(json.loads(body)['scope'], decoded_scope) # implicit grant diff --git a/tests/oauth2/rfc6749/grant_types/test_openid_connect.py b/tests/oauth2/rfc6749/grant_types/test_openid_connect.py index f10d36cf..573d491d 100644 --- a/tests/oauth2/rfc6749/grant_types/test_openid_connect.py +++ b/tests/oauth2/rfc6749/grant_types/test_openid_connect.py @@ -6,7 +6,11 @@ import mock from oauthlib.common import Request -from oauthlib.oauth2.rfc6749.grant_types import (OIDCNoPrompt, +from oauthlib.oauth2.rfc6749.grant_types import (AuthTokenGrantDispatcher, + AuthorizationCodeGrant, + ImplicitGrant, + ImplicitTokenGrantDispatcher, + OIDCNoPrompt, OpenIDConnectAuthCode, OpenIDConnectHybrid, OpenIDConnectImplicit) @@ -24,6 +28,7 @@ def setUp(self): super(OpenIDAuthCodeInterferenceTest, self).setUp() self.auth = OpenIDConnectAuthCode(request_validator=self.mock_validator) + class OpenIDImplicitInterferenceTest(ImplicitGrantTest): """Test that OpenID don't interfere with normal OAuth 2 flows.""" @@ -270,6 +275,7 @@ def setUp(self): self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' + class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): def setUp(self): @@ -280,6 +286,7 @@ def setUp(self): self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token + class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): def setUp(self): @@ -289,3 +296,108 @@ def setUp(self): token = 'MOCKED_TOKEN' self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + + +class ImplicitTokenGrantDispatcherTest(TestCase): + def setUp(self): + self.request = Request('http://a.b/path') + request_validator = mock.MagicMock() + implicit_grant = ImplicitGrant(request_validator) + openid_connect_implicit = OpenIDConnectImplicit(request_validator) + + self.dispatcher = ImplicitTokenGrantDispatcher( + default_implicit_grant=implicit_grant, + oidc_implicit_grant=openid_connect_implicit + ) + + def test_create_authorization_response_openid(self): + self.request.scopes = ('hello', 'openid') + self.request.response_type = 'id_token' + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, OpenIDConnectImplicit)) + + def test_validate_authorization_request_openid(self): + self.request.scopes = ('hello', 'openid') + self.request.response_type = 'id_token' + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, OpenIDConnectImplicit)) + + def test_create_authorization_response_oauth(self): + self.request.scopes = ('hello', 'world') + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, ImplicitGrant)) + + def test_validate_authorization_request_oauth(self): + self.request.scopes = ('hello', 'world') + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, ImplicitGrant)) + + +class DispatcherTest(TestCase): + def setUp(self): + self.request = Request('http://a.b/path') + self.request.decoded_body = ( + ("client_id", "me"), + ("code", "code"), + ("redirect_url", "https://a.b/cb"), + ) + + self.request_validator = mock.MagicMock() + self.auth_grant = AuthorizationCodeGrant(self.request_validator) + self.openid_connect_auth = OpenIDConnectAuthCode(self.request_validator) + + +class AuthTokenGrantDispatcherOpenIdTest(DispatcherTest): + + def setUp(self): + super(AuthTokenGrantDispatcherOpenIdTest, self).setUp() + self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid') + self.dispatcher = AuthTokenGrantDispatcher( + self.request_validator, + default_token_grant=self.auth_grant, + oidc_token_grant=self.openid_connect_auth + ) + + def test_create_token_response_openid(self): + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, OpenIDConnectAuthCode)) + self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) + + +class AuthTokenGrantDispatcherOpenIdWithoutCodeTest(DispatcherTest): + + def setUp(self): + super(AuthTokenGrantDispatcherOpenIdWithoutCodeTest, self).setUp() + self.request.decoded_body = ( + ("client_id", "me"), + ("code", ""), + ("redirect_url", "https://a.b/cb"), + ) + self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid') + self.dispatcher = AuthTokenGrantDispatcher( + self.request_validator, + default_token_grant=self.auth_grant, + oidc_token_grant=self.openid_connect_auth + ) + + def test_create_token_response_openid_without_code(self): + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, AuthorizationCodeGrant)) + self.assertFalse(self.dispatcher.request_validator.get_authorization_code_scopes.called) + + +class AuthTokenGrantDispatcherOAuthTest(DispatcherTest): + + def setUp(self): + super(AuthTokenGrantDispatcherOAuthTest, self).setUp() + self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'world') + self.dispatcher = AuthTokenGrantDispatcher( + self.request_validator, + default_token_grant=self.auth_grant, + oidc_token_grant=self.openid_connect_auth + ) + + def test_create_token_response_oauth(self): + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, AuthorizationCodeGrant)) + self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) diff --git a/tests/oauth2/rfc6749/test_server.py b/tests/oauth2/rfc6749/test_server.py index 305b795e..da303ceb 100644 --- a/tests/oauth2/rfc6749/test_server.py +++ b/tests/oauth2/rfc6749/test_server.py @@ -279,7 +279,7 @@ def set_user(request): @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): - body = 'grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' + body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' headers, body, status_code = self.endpoint.create_token_response( '', body=body) body = json.loads(body) @@ -293,7 +293,7 @@ def test_authorization_grant(self): } self.assertEqual(body, token) - body = 'grant_type=authorization_code&code=abc&state=xyz' + body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&state=xyz' headers, body, status_code = self.endpoint.create_token_response( '', body=body) body = json.loads(body) @@ -349,12 +349,12 @@ def test_client_grant(self): self.assertEqual(body, token) def test_missing_type(self): - _, body, _ = self.endpoint.create_token_response('', body='') + _, body, _ = self.endpoint.create_token_response('', body='client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&code=abc') token = {'error': 'unsupported_grant_type'} self.assertEqual(json.loads(body), token) def test_invalid_type(self): - body = 'grant_type=invalid' + body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=invalid&code=abc' _, body, _ = self.endpoint.create_token_response('', body=body) token = {'error': 'unsupported_grant_type'} self.assertEqual(json.loads(body), token) From c6b11373648af4b81367b0424b65b15ee8b58261 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 18 Oct 2017 23:40:32 +0900 Subject: [PATCH 003/237] Refactor OAuth2Error --- oauthlib/oauth2/rfc6749/errors.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index e0c29a03..180f6363 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -18,8 +18,8 @@ class OAuth2Error(Exception): status_code = 400 description = '' - def __init__(self, description=None, uri=None, state=None, status_code=None, - request=None): + def __init__(self, description=None, uri=None, state=None, + status_code=None, request=None): """ description: A human-readable ASCII [USASCII] text providing additional information, used to assist the client @@ -39,8 +39,9 @@ def __init__(self, description=None, uri=None, state=None, status_code=None, request: Oauthlib Request object """ - self.response_mode = None - self.description = description or self.description + if description is not None: + self.description = description + message = '(%s) %s' % (self.error, self.description) if request: message += ' ' + repr(request) @@ -61,10 +62,17 @@ def __init__(self, description=None, uri=None, state=None, status_code=None, self.grant_type = request.grant_type if not state: self.state = request.state + else: + self.redirect_uri = None + self.client_id = None + self.scopes = None + self.response_type = None + self.response_mode = None + self.grant_type = None def in_uri(self, uri): - return add_params_to_uri(uri, self.twotuples, - fragment=self.response_mode == "fragment") + fragment = self.response_mode == "fragment" + return add_params_to_uri(uri, self.twotuples, fragment) @property def twotuples(self): From c1eda9f1c92f7110ad41af5e48864bfb2ac37c7c Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Wed, 18 Oct 2017 23:57:51 +0900 Subject: [PATCH 004/237] Fix travis for PyPy This is due to https://github.com/pyca/cryptography/pull/3970 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b66a5767..ee88674b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ matrix: - python: pypy-5.3 env: TOXENV=pypy -install: pip install tox coveralls +install: pip install cryptography==2.0.3 tox coveralls script: tox after_success: coveralls From 1dcea0406affbcbf28e6e8f4a5307adf6bca46d1 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 19 Oct 2017 00:09:25 +0900 Subject: [PATCH 005/237] Try another way to fix travis --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ee88674b..4dee48bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,9 @@ matrix: - python: pypy-5.3 env: TOXENV=pypy -install: pip install cryptography==2.0.3 tox coveralls +install: + - pip install -U setuptools + - pip install tox coveralls script: tox after_success: coveralls From fb7ec207b17e0cacf52f9f3c2643c4b9036d827c Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Thu, 19 Oct 2017 00:27:41 +0900 Subject: [PATCH 006/237] Version bump 2.0.5 --- CHANGELOG.rst | 6 ++++++ oauthlib/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 397fc07c..8a20f92d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Changelog ========= +2.0.5 (2017-10-19) +------------------ + +* Fix OAuth2Error.response_mode for #463. +* Documentation improvement. + 2.0.4 (2017-09-17) ------------------ * Fixed typo that caused OAuthlib to crash because of the fix in "Address missing OIDC errors and fix a typo in the AccountSelectionRequired exception". diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 91215827..459f307e 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -10,7 +10,7 @@ """ __author__ = 'Idan Gazit ' -__version__ = '2.0.4' +__version__ = '2.0.5' import logging From 14215244249f2d8df73ec47dbab5db1efd0fc2f4 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 24 Oct 2017 06:14:47 -0700 Subject: [PATCH 007/237] Include license file in the generated wheel package (#494) The wheel package format supports including the license file. This is done using the [metadata] section in the setup.cfg file. For additional information on this feature, see: https://wheel.readthedocs.io/en/stable/index.html#including-the-license-in-the-generated-wheel-file --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 2a9acf13..ed8a958e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,5 @@ [bdist_wheel] universal = 1 + +[metadata] +license_file = LICENSE From 4b85d90a54572f54b8b19d036d76d043cf116699 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 27 Oct 2017 08:56:28 -0700 Subject: [PATCH 008/237] When deploying a release to PyPI, include the wheel distribution (#496) For Travis CI documentation on including a bdist_wheel distribution, see: https://docs.travis-ci.com/user/deployment/pypi/#Uploading-different-distributions Fixes #493 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 4dee48bb..f5e9aca5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,7 @@ deploy: user: ib.lundgren password: secure: PGZF9pRiTGCSwQjk1ddTKF3x4rQ0iAiPbg2uSixyO68uMXRgJjwHhSrNM0OEqtK5YWU5FE5L0DwR1nkrpEJKO4a5q2EOgos+gVoKpJfinoUNOOkjc1VHpqKM0uRf/OKrw1alvWUwqvW8B+DOb9TY5c5VZxQuRL+iwdrtwzFlKls= + distributions: sdist bdist_wheel on: tags: true repo: idan/oauthlib From fa0b63cfaced831d8b916c5a125128f582acf044 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Tue, 14 Nov 2017 23:38:33 +0800 Subject: [PATCH 009/237] Check access token in self.token dict (#500) * Check access token in self.token dict * fix typo --- oauthlib/oauth2/rfc6749/clients/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index c2f8809f..5c5aceeb 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -186,7 +186,7 @@ def add_token(self, uri, http_method='GET', body=None, headers=None, if not self.token_type.lower() in case_insensitive_token_types: raise ValueError("Unsupported token type: %s" % self.token_type) - if not self.access_token: + if not (self.access_token or self.token.get('access_token')): raise ValueError("Missing access token.") if self._expires_at and self._expires_at < time.time(): From cfb82feb03fcd60b3b66ac09bf1b478cd5f11b7d Mon Sep 17 00:00:00 2001 From: Viktor Haag Date: Tue, 14 Nov 2017 07:44:44 -0800 Subject: [PATCH 010/237] Add support for HMAC-SHA256 (builds on PR#388) (#498) * Add support for HMAC-SHA256 * Add explicit declaration of HMAC-SHA1 and point HMAC at it To avoid confusion, HMAC constant name should explicitly state which SHA variant is used, but for backwards compatibility, SIGNATURE_HMAC is still needed * add support for HMAC-SHA256 including tests and comments * constructor tests verify client built with correct signer method --- oauthlib/oauth1/__init__.py | 2 +- oauthlib/oauth1/rfc5849/__init__.py | 11 ++++-- oauthlib/oauth1/rfc5849/signature.py | 57 ++++++++++++++++++++++++++++ tests/oauth1/rfc5849/test_client.py | 40 ++++++++++++++++++- 4 files changed, 103 insertions(+), 7 deletions(-) diff --git a/oauthlib/oauth1/__init__.py b/oauthlib/oauth1/__init__.py index f9dff74d..dc908d4e 100644 --- a/oauthlib/oauth1/__init__.py +++ b/oauthlib/oauth1/__init__.py @@ -9,7 +9,7 @@ from __future__ import absolute_import, unicode_literals from .rfc5849 import Client -from .rfc5849 import SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT +from .rfc5849 import SIGNATURE_HMAC, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_RSA, SIGNATURE_PLAINTEXT from .rfc5849 import SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_QUERY from .rfc5849 import SIGNATURE_TYPE_BODY from .rfc5849.request_validator import RequestValidator diff --git a/oauthlib/oauth1/rfc5849/__init__.py b/oauthlib/oauth1/rfc5849/__init__.py index 06902e20..f9113ab5 100644 --- a/oauthlib/oauth1/rfc5849/__init__.py +++ b/oauthlib/oauth1/rfc5849/__init__.py @@ -27,10 +27,12 @@ from oauthlib.common import generate_timestamp, to_unicode from . import parameters, signature -SIGNATURE_HMAC = "HMAC-SHA1" +SIGNATURE_HMAC_SHA1 = "HMAC-SHA1" +SIGNATURE_HMAC_SHA256 = "HMAC-SHA256" +SIGNATURE_HMAC = SIGNATURE_HMAC_SHA1 SIGNATURE_RSA = "RSA-SHA1" SIGNATURE_PLAINTEXT = "PLAINTEXT" -SIGNATURE_METHODS = (SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT) +SIGNATURE_METHODS = (SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_RSA, SIGNATURE_PLAINTEXT) SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER' SIGNATURE_TYPE_QUERY = 'QUERY' @@ -43,7 +45,8 @@ class Client(object): """A client used to sign OAuth 1.0 RFC 5849 requests.""" SIGNATURE_METHODS = { - SIGNATURE_HMAC: signature.sign_hmac_sha1_with_client, + SIGNATURE_HMAC_SHA1: signature.sign_hmac_sha1_with_client, + SIGNATURE_HMAC_SHA256: signature.sign_hmac_sha256_with_client, SIGNATURE_RSA: signature.sign_rsa_sha1_with_client, SIGNATURE_PLAINTEXT: signature.sign_plaintext_with_client } @@ -57,7 +60,7 @@ def __init__(self, client_key, resource_owner_key=None, resource_owner_secret=None, callback_uri=None, - signature_method=SIGNATURE_HMAC, + signature_method=SIGNATURE_HMAC_SHA1, signature_type=SIGNATURE_TYPE_AUTH_HEADER, rsa_key=None, verifier=None, realm=None, encoding='utf-8', decoding=None, diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index 10d057f8..30001efd 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -469,6 +469,63 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret): # .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8 return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8') + +def sign_hmac_sha256_with_client(base_string, client): + return sign_hmac_sha256(base_string, + client.client_secret, + client.resource_owner_secret + ) + + +def sign_hmac_sha256(base_string, client_secret, resource_owner_secret): + """**HMAC-SHA256** + + The "HMAC-SHA256" signature method uses the HMAC-SHA256 signature + algorithm as defined in `RFC4634`_:: + + digest = HMAC-SHA256 (key, text) + + Per `section 3.4.2`_ of the spec. + + .. _`RFC4634`: http://tools.ietf.org/html/rfc4634 + .. _`section 3.4.2`: http://tools.ietf.org/html/rfc5849#section-3.4.2 + """ + + # The HMAC-SHA256 function variables are used in following way: + + # text is set to the value of the signature base string from + # `Section 3.4.1.1`_. + # + # .. _`Section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1 + text = base_string + + # key is set to the concatenated values of: + # 1. The client shared-secret, after being encoded (`Section 3.6`_). + # + # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + key = utils.escape(client_secret or '') + + # 2. An "&" character (ASCII code 38), which MUST be included + # even when either secret is empty. + key += '&' + + # 3. The token shared-secret, after being encoded (`Section 3.6`_). + # + # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + key += utils.escape(resource_owner_secret or '') + + # FIXME: HMAC does not support unicode! + key_utf8 = key.encode('utf-8') + text_utf8 = text.encode('utf-8') + signature = hmac.new(key_utf8, text_utf8, hashlib.sha256) + + # digest is used to set the value of the "oauth_signature" protocol + # parameter, after the result octet string is base64-encoded + # per `RFC2045, Section 6.8`. + # + # .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8 + return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8') + _jwtrs1 = None #jwt has some nice pycrypto/cryptography abstractions diff --git a/tests/oauth1/rfc5849/test_client.py b/tests/oauth1/rfc5849/test_client.py index dcb4c3d2..777efc2d 100644 --- a/tests/oauth1/rfc5849/test_client.py +++ b/tests/oauth1/rfc5849/test_client.py @@ -2,7 +2,8 @@ from __future__ import absolute_import, unicode_literals from oauthlib.common import Request -from oauthlib.oauth1 import (SIGNATURE_PLAINTEXT, SIGNATURE_RSA, +from oauthlib.oauth1 import (SIGNATURE_PLAINTEXT, SIGNATURE_HMAC_SHA1, + SIGNATURE_HMAC_SHA256, SIGNATURE_RSA, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY) from oauthlib.oauth1.rfc5849 import Client, bytes_type @@ -62,13 +63,48 @@ def test_decoding(self): self.assertIsInstance(k, bytes_type) self.assertIsInstance(v, bytes_type) + def test_hmac_sha1(self): + client = Client('client_key') + # instance is using the correct signer method + self.assertEqual(Client.SIGNATURE_METHODS[SIGNATURE_HMAC_SHA1], + client.SIGNATURE_METHODS[client.signature_method]) + + def test_hmac_sha256(self): + client = Client('client_key', signature_method=SIGNATURE_HMAC_SHA256) + # instance is using the correct signer method + self.assertEqual(Client.SIGNATURE_METHODS[SIGNATURE_HMAC_SHA256], + client.SIGNATURE_METHODS[client.signature_method]) + def test_rsa(self): client = Client('client_key', signature_method=SIGNATURE_RSA) - self.assertIsNone(client.rsa_key) # don't need an RSA key to instantiate + # instance is using the correct signer method + self.assertEqual(Client.SIGNATURE_METHODS[SIGNATURE_RSA], + client.SIGNATURE_METHODS[client.signature_method]) + # don't need an RSA key to instantiate + self.assertIsNone(client.rsa_key) class SignatureMethodTest(TestCase): + def test_hmac_sha1_method(self): + client = Client('client_key', timestamp='1234567890', nonce='abc') + u, h, b = client.sign('http://example.com') + correct = ('OAuth oauth_nonce="abc", oauth_timestamp="1234567890", ' + 'oauth_version="1.0", oauth_signature_method="HMAC-SHA1", ' + 'oauth_consumer_key="client_key", ' + 'oauth_signature="hH5BWYVqo7QI4EmPBUUe9owRUUQ%3D"') + self.assertEqual(h['Authorization'], correct) + + def test_hmac_sha256_method(self): + client = Client('client_key', signature_method=SIGNATURE_HMAC_SHA256, + timestamp='1234567890', nonce='abc') + u, h, b = client.sign('http://example.com') + correct = ('OAuth oauth_nonce="abc", oauth_timestamp="1234567890", ' + 'oauth_version="1.0", oauth_signature_method="HMAC-SHA256", ' + 'oauth_consumer_key="client_key", ' + 'oauth_signature="JzgJWBxX664OiMW3WE4MEjtYwOjI%2FpaUWHqtdHe68Es%3D"') + self.assertEqual(h['Authorization'], correct) + def test_rsa_method(self): private_key = ( "-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQDk1/bxy" From 9b95e4e8f094d78abe577203ad1ef53aecfdb270 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 8 Nov 2017 09:55:03 +0100 Subject: [PATCH 011/237] Added initial introspect support --- docs/feature_matrix.rst | 1 + docs/oauth2/endpoints/endpoints.rst | 6 +- docs/oauth2/endpoints/introspect.rst | 26 ++++ oauthlib/oauth2/__init__.py | 1 + oauthlib/oauth2/rfc6749/endpoints/__init__.py | 1 + .../oauth2/rfc6749/endpoints/introspect.py | 135 ++++++++++++++++++ .../rfc6749/endpoints/pre_configured.py | 27 ++-- oauthlib/oauth2/rfc6749/errors.py | 2 +- oauthlib/oauth2/rfc6749/request_validator.py | 20 +++ .../endpoints/test_introspect_endpoint.py | 132 +++++++++++++++++ 10 files changed, 339 insertions(+), 12 deletions(-) create mode 100644 docs/oauth2/endpoints/introspect.rst create mode 100644 oauthlib/oauth2/rfc6749/endpoints/introspect.py create mode 100644 tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py diff --git a/docs/feature_matrix.rst b/docs/feature_matrix.rst index 0f9021da..59f3f3ab 100644 --- a/docs/feature_matrix.rst +++ b/docs/feature_matrix.rst @@ -17,6 +17,7 @@ OAuth 2 client and provider support for - Bearer Tokens - Draft MAC tokens - Token Revocation +- Token Introspection - OpenID Connect Authentication with support for SAML2 and JWT tokens, dynamic client registration and more to diff --git a/docs/oauth2/endpoints/endpoints.rst b/docs/oauth2/endpoints/endpoints.rst index 0e707986..5f7ae8c6 100644 --- a/docs/oauth2/endpoints/endpoints.rst +++ b/docs/oauth2/endpoints/endpoints.rst @@ -14,11 +14,12 @@ client attempts to access the user resources on their behalf. :maxdepth: 2 authorization + introspect token resource revocation -There are three different endpoints, the authorization endpoint which mainly +There are three main endpoints, the authorization endpoint which mainly handles user authorization, the token endpoint which provides tokens and the resource endpoint which provides access to protected resources. It is to the endpoints you will feed requests and get back an almost complete response. This @@ -27,3 +28,6 @@ later. The main purpose of the endpoint in OAuthLib is to figure out which grant type or token to dispatch the request to. + +Then, you can extend your OAuth implementation by proposing introspect or +revocation endpoints. diff --git a/docs/oauth2/endpoints/introspect.rst b/docs/oauth2/endpoints/introspect.rst new file mode 100644 index 00000000..53ade8b2 --- /dev/null +++ b/docs/oauth2/endpoints/introspect.rst @@ -0,0 +1,26 @@ +=================== +Token introspection +=================== + +Introspect endpoints read opaque access and/or refresh tokens upon client +request. Also known as tokeninfo. + +.. code-block:: python + + # Initial setup + from your_validator import your_validator + server = WebApplicationServer(your_validator) + + # Token revocation + uri = 'https://example.com/introspect' + headers, body, http_method = {}, 'token=sldafh309sdf', 'POST' + + headers, body, status = server.create_introspect_response(uri, + headers=headers, body=body, http_method=http_method) + + from your_framework import http_response + http_response(body, status=status, headers=headers) + + +.. autoclass:: oauthlib.oauth2.IntrospectEndpoint + :members: diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index c8d934e9..dc7b431c 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -15,6 +15,7 @@ from .rfc6749.clients import BackendApplicationClient from .rfc6749.clients import ServiceApplicationClient from .rfc6749.endpoints import AuthorizationEndpoint +from .rfc6749.endpoints import IntrospectEndpoint from .rfc6749.endpoints import TokenEndpoint from .rfc6749.endpoints import ResourceEndpoint from .rfc6749.endpoints import RevocationEndpoint diff --git a/oauthlib/oauth2/rfc6749/endpoints/__init__.py b/oauthlib/oauth2/rfc6749/endpoints/__init__.py index 848bec6b..9557f92a 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/__init__.py +++ b/oauthlib/oauth2/rfc6749/endpoints/__init__.py @@ -9,6 +9,7 @@ from __future__ import absolute_import, unicode_literals from .authorization import AuthorizationEndpoint +from .introspect import IntrospectEndpoint from .token import TokenEndpoint from .resource import ResourceEndpoint from .revocation import RevocationEndpoint diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py new file mode 100644 index 00000000..7613acce --- /dev/null +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749.endpoint.introspect +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An implementation of the OAuth 2.0 `Token Introspection`. + +.. _`Token Introspection`: https://tools.ietf.org/html/rfc7662 +""" +from __future__ import absolute_import, unicode_literals + +import json +import logging + +from oauthlib.common import Request + +from ..errors import (InvalidClientError, InvalidRequestError, OAuth2Error, + UnsupportedTokenTypeError) +from .base import BaseEndpoint, catch_errors_and_unavailability + +log = logging.getLogger(__name__) + + +class IntrospectEndpoint(BaseEndpoint): + + """Introspect token endpoint. + + This endpoint defines a method to query an OAuth 2.0 authorization + server to determine the active state of an OAuth 2.0 token and to + determine meta-information about this token. OAuth 2.0 deployments + can use this method to convey information about the authorization + context of the token from the authorization server to the protected + resource. + + To prevent the values of access tokens from leaking into + server-side logs via query parameters, an authorization server + offering token introspection MAY disallow the use of HTTP GET on + the introspection endpoint and instead require the HTTP POST method + to be used at the introspection endpoint. + """ + + valid_token_types = ('access_token', 'refresh_token') + + def __init__(self, request_validator, supported_token_types=None): + BaseEndpoint.__init__(self) + self.request_validator = request_validator + self.supported_token_types = ( + supported_token_types or self.valid_token_types) + + @catch_errors_and_unavailability + def create_introspect_response(self, uri, http_method='POST', body=None, + headers=None): + """Create introspect valid or invalid response + + If the authorization server is unable to determine the state + of the token without additional information, it SHOULD return + an introspection response indicating the token is not active + as described in Section 2.2. + """ + request = Request(uri, http_method, body, headers) + try: + self.validate_introspect_request(request) + log.debug('Token introspect valid for %r.', request) + except OAuth2Error as e: + log.debug('Client error during validation of %r. %r.', request, e) + return {}, e.json, e.status_code + + claims = self.request_validator.introspect_token( + request.token, + request.token_type_hint, + request + ) + headers = { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } + if claims is None: + return headers, json.dumps(dict(active=False)), 200 + if "active" in claims: + claims.pop("active") + return headers, json.dumps(dict(active=True, **claims)), 200 + + def validate_introspect_request(self, request): + """Ensure the request is valid. + + The protected resource calls the introspection endpoint using + an HTTP POST request with parameters sent as + "application/x-www-form-urlencoded". + + token REQUIRED. The string value of the token. + + token_type_hint OPTIONAL. + A hint about the type of the token submitted for + introspection. The protected resource MAY pass this parameter to + help the authorization server optimize the token lookup. If the + server is unable to locate the token using the given hint, it MUST + extend its search across all of its supported token types. An + authorization server MAY ignore this parameter, particularly if it + is able to detect the token type automatically. + * access_token: An Access Token as defined in [`RFC6749`], + `section 1.4`_ + + * refresh_token: A Refresh Token as defined in [`RFC6749`], + `section 1.5`_ + + The introspection endpoint MAY accept other OPTIONAL + parameters to provide further context to the query. For + instance, an authorization server may desire to know the IP + address of the client accessing the protected resource to + determine if the correct client is likely to be presenting the + token. The definition of this or any other parameters are + outside the scope of this specification, to be defined by + service documentation or extensions to this specification. + + .. _`section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4 + .. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5 + .. _`RFC6749`: http://tools.ietf.org/html/rfc6749 + """ + if not request.token: + raise InvalidRequestError(request=request, + description='Missing token parameter.') + + if self.request_validator.client_authentication_required(request): + if not self.request_validator.authenticate_client(request): + log.debug('Client authentication failed, %r.', request) + raise InvalidClientError(request=request) + elif not self.request_validator.authenticate_client_id(request.client_id, request): + log.debug('Client authentication failed, %r.', request) + raise InvalidClientError(request=request) + + if (request.token_type_hint and + request.token_type_hint in self.valid_token_types and + request.token_type_hint not in self.supported_token_types): + raise UnsupportedTokenTypeError(request=request) diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index 07c37158..f1dfead9 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -18,13 +18,14 @@ ResourceOwnerPasswordCredentialsGrant) from ..tokens import BearerToken from .authorization import AuthorizationEndpoint +from .introspect import IntrospectEndpoint from .resource import ResourceEndpoint from .revocation import RevocationEndpoint from .token import TokenEndpoint -class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring all four major grant types.""" @@ -88,10 +89,11 @@ def __init__(self, request_validator, token_expires_in=None, ResourceEndpoint.__init__(self, default_token='Bearer', token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator) -class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class WebApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" @@ -126,10 +128,11 @@ def __init__(self, request_validator, token_generator=None, ResourceEndpoint.__init__(self, default_token='Bearer', token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator) -class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint, - RevocationEndpoint): +class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Implicit code grant and Bearer tokens.""" @@ -159,10 +162,11 @@ def __init__(self, request_validator, token_generator=None, token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator, supported_token_types=['access_token']) + IntrospectEndpoint.__init__(self, request_validator) -class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens.""" @@ -195,10 +199,11 @@ def __init__(self, request_validator, token_generator=None, ResourceEndpoint.__init__(self, default_token='Bearer', token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator) -class BackendApplicationServer(TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class BackendApplicationServer(TokenEndpoint, IntrospectEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring Client Credentials grant and Bearer tokens.""" @@ -228,3 +233,5 @@ def __init__(self, request_validator, token_generator=None, token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator, supported_token_types=['access_token']) + IntrospectEndpoint.__init__(self, request_validator, + supported_token_types=['access_token']) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 180f6363..1d5e98d5 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -267,7 +267,7 @@ class UnsupportedGrantTypeError(OAuth2Error): class UnsupportedTokenTypeError(OAuth2Error): """ - The authorization server does not support the revocation of the + The authorization server does not support the hint of the presented token type. I.e. the client tried to revoke an access token on a server not supporting this feature. """ diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index ba129d5e..525ba33d 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -166,6 +166,26 @@ def is_within_original_scope(self, request_scopes, refresh_token, request, *args """ return False + def introspect_token(self, token, token_type_hint, request, *args, **kwargs): + """Introspect an access or refresh token. + + Called once introspect token request is validated. This method + should return a dictionary with any desired claims associated + with the *token*. The implementation can use *token_type_hint* + to lookup this type first, but then it must fallback to other + types known, to be compliant with RFC. + + The dict of claims is added to request.token after this method. + + :param token: The token string. + :param token_type_hint: access_token or refresh_token. + :param request: The HTTP Request (oauthlib.common.Request) + + Method is used by: + - Introspect Endpoint (all grants are compatible) + """ + raise NotImplementedError('Subclasses must implement this method.') + def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs): """Invalidate an authorization code after use. diff --git a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py new file mode 100644 index 00000000..7ec81905 --- /dev/null +++ b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +from json import loads + +from mock import MagicMock + +from oauthlib.common import urlencode +from oauthlib.oauth2 import RequestValidator, IntrospectEndpoint + +from ....unittest import TestCase + + +class IntrospectEndpointTest(TestCase): + + def setUp(self): + self.validator = MagicMock(wraps=RequestValidator()) + self.validator.client_authentication_required.return_value = True + self.validator.authenticate_client.return_value = True + self.validator.validate_bearer_token.return_value = True + self.validator.introspect_token.return_value = {} + self.endpoint = IntrospectEndpoint(self.validator) + + self.uri = 'should_not_matter' + self.headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + self.resp_h = { + 'Cache-Control': 'no-store', + 'Content-Type': 'application/json', + 'Pragma': 'no-cache' + } + self.resp_b = { + "active": True + } + + def test_introspect_token(self): + for token_type in ('access_token', 'refresh_token', 'invalid'): + body = urlencode([('token', 'foo'), + ('token_type_hint', token_type)]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), self.resp_b) + self.assertEqual(s, 200) + + def test_introspect_token_nohint(self): + # don't specify token_type_hint + body = urlencode([('token', 'foo')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), self.resp_b) + self.assertEqual(s, 200) + + def test_introspect_token_false(self): + self.validator.introspect_token.return_value = None + body = urlencode([('token', 'foo')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), {"active": False}) + self.assertEqual(s, 200) + + def test_introspect_token_claims(self): + self.validator.introspect_token.return_value = {"foo": "bar"} + body = urlencode([('token', 'foo')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), {"active": True, "foo": "bar"}) + self.assertEqual(s, 200) + + def test_introspect_token_claims_spoof_active(self): + self.validator.introspect_token.return_value = {"foo": "bar", "active": False} + body = urlencode([('token', 'foo')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), {"active": True, "foo": "bar"}) + self.assertEqual(s, 200) + + def test_introspect_token_client_authentication_failed(self): + self.validator.authenticate_client.return_value = False + body = urlencode([('token', 'foo'), + ('token_type_hint', 'access_token')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, {}) + self.assertEqual(loads(b)['error'], 'invalid_client') + self.assertEqual(s, 401) + + def test_introspect_token_public_client_authentication(self): + self.validator.client_authentication_required.return_value = False + self.validator.authenticate_client_id.return_value = True + for token_type in ('access_token', 'refresh_token', 'invalid'): + body = urlencode([('token', 'foo'), + ('token_type_hint', token_type)]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, self.resp_h) + self.assertEqual(loads(b), self.resp_b) + self.assertEqual(s, 200) + + def test_introspect_token_public_client_authentication_failed(self): + self.validator.client_authentication_required.return_value = False + self.validator.authenticate_client_id.return_value = False + body = urlencode([('token', 'foo'), + ('token_type_hint', 'access_token')]) + h, b, s = self.endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, {}) + self.assertEqual(loads(b)['error'], 'invalid_client') + self.assertEqual(s, 401) + + + def test_introspect_unsupported_token(self): + endpoint = IntrospectEndpoint(self.validator, + supported_token_types=['access_token']) + body = urlencode([('token', 'foo'), + ('token_type_hint', 'refresh_token')]) + h, b, s = endpoint.create_introspect_response(self.uri, + headers=self.headers, body=body) + self.assertEqual(h, {}) + self.assertEqual(loads(b)['error'], 'unsupported_token_type') + self.assertEqual(s, 400) + + h, b, s = endpoint.create_introspect_response(self.uri, + headers=self.headers, body='') + self.assertEqual(h, {}) + self.assertEqual(loads(b)['error'], 'invalid_request') + self.assertEqual(s, 400) From ef8a3b47305b23b278310c1f21106c677a748434 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 19 Dec 2017 15:18:48 +0100 Subject: [PATCH 012/237] Added default supported_token_types for Mobile --- oauthlib/oauth2/rfc6749/endpoints/pre_configured.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index f1dfead9..378339ac 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -162,7 +162,8 @@ def __init__(self, request_validator, token_generator=None, token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator, supported_token_types=['access_token']) - IntrospectEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator, + supported_token_types=['access_token']) class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint, From 296c6bc5931c95f631c1a496dacc523959fc50e9 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 19 Dec 2017 15:19:09 +0100 Subject: [PATCH 013/237] Improved doc by adding links to RFC and list of claims. --- oauthlib/oauth2/rfc6749/request_validator.py | 30 ++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 525ba33d..4b76b7a9 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -169,11 +169,28 @@ def is_within_original_scope(self, request_scopes, refresh_token, request, *args def introspect_token(self, token, token_type_hint, request, *args, **kwargs): """Introspect an access or refresh token. - Called once introspect token request is validated. This method - should return a dictionary with any desired claims associated - with the *token*. The implementation can use *token_type_hint* - to lookup this type first, but then it must fallback to other - types known, to be compliant with RFC. + Called once the introspect request is validated. This method should + verify the *token* and either return a dictionary with the list of + claims associated, or `None` in case the token is unknown. + + Below the list of registered claims you should be interested in: + - scope : space-separated list of scopes + - client_id : client identifier + - username : human-readable identifier for the resource owner + - token_type : type of the token + - exp : integer timestamp indicating when this token will expire + - iat : integer timestamp indicating when this token was issued + - nbf : integer timestamp indicating when it can be "not-before" used + - sub : subject of the token - identifier of the resource owner + - aud : list of string identifiers representing the intended audience + - iss : string representing issuer of this token + - jti : string identifier for the token + + Note that most of them are coming directly from JWT RFC. More details + can be found in `Introspect Claims`_ or `_JWT Claims`_. + + The implementation can use *token_type_hint* to improve lookup + efficency, but must fallback to other types to be compliant with RFC. The dict of claims is added to request.token after this method. @@ -183,6 +200,9 @@ def introspect_token(self, token, token_type_hint, request, *args, **kwargs): Method is used by: - Introspect Endpoint (all grants are compatible) + + .. _`Introspect Claims`: https://tools.ietf.org/html/rfc7662#section-2.2 + .. _`JWT Claims`: https://tools.ietf.org/html/rfc7519#section-4 """ raise NotImplementedError('Subclasses must implement this method.') From 66d7296229122536163beabcc9552a0d8debbf60 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 2 Jan 2018 17:00:03 +0100 Subject: [PATCH 014/237] Added bottle-oauthlib (#509) --- README.rst | 2 ++ docs/faq.rst | 17 +++++++++++++---- docs/oauth2/endpoints/endpoints.rst | 2 +- docs/oauth2/server.rst | 8 ++++++-- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index eb85ffab..656d72c3 100644 --- a/README.rst +++ b/README.rst @@ -56,6 +56,7 @@ The following packages provide OAuth support using OAuthLib. - For Django there is `django-oauth-toolkit`_, which includes `Django REST framework`_ support. - For Flask there is `flask-oauthlib`_ and `Flask-Dance`_. - For Pyramid there is `pyramid-oauthlib`_. +- For Bottle there is `bottle-oauthlib`_. If you have written an OAuthLib package that supports your favorite framework, please open a Pull Request, updating the documentation. @@ -65,6 +66,7 @@ please open a Pull Request, updating the documentation. .. _`Django REST framework`: http://django-rest-framework.org .. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance .. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib +.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib Using OAuthLib? Please get in touch! ------------------------------------ diff --git a/docs/faq.rst b/docs/faq.rst index 4d896f54..0c61af9d 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -65,10 +65,17 @@ How do I use OAuthLib with Google, Twitter and other providers? How do I use OAuthlib as a provider with Django, Flask and other web frameworks? -------------------------------------------------------------------------------- - Providers using Django should seek out `django-oauth-toolkit`_ - and those using Flask `flask-oauthlib`_. For other frameworks, - please get in touch by opening a `GitHub issue`_, on `G+`_ or - on IRC #oauthlib irc.freenode.net. + Providers can be implemented in any web frameworks. However, some of + them have ready-to-use libraries to help integration: + - Django `django-oauth-toolkit`_ + - Flask `flask-oauthlib`_ + - Pyramid `pyramid-oauthlib`_ + - Bottle `bottle-oauthlib`_ + + For other frameworks, please get in touch by opening a `GitHub issue`_, on `G+`_ or + on IRC #oauthlib irc.freenode.net. If you have written an OAuthLib package that + supports your favorite framework, please open a Pull Request to update the docs. + What is the difference between authentication and authorization? ---------------------------------------------------------------- @@ -91,6 +98,8 @@ Some argue OAuth 2 is worse than 1, is that true? .. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib .. _`django-oauth-toolkit`: https://github.com/evonove/django-oauth-toolkit .. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib +.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib +.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib .. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new .. _`G+`: https://plus.google.com/communities/101889017375384052571 .. _`difference`: http://www.cyberciti.biz/faq/authentication-vs-authorization/ diff --git a/docs/oauth2/endpoints/endpoints.rst b/docs/oauth2/endpoints/endpoints.rst index 0e707986..9bd1c4ec 100644 --- a/docs/oauth2/endpoints/endpoints.rst +++ b/docs/oauth2/endpoints/endpoints.rst @@ -23,7 +23,7 @@ handles user authorization, the token endpoint which provides tokens and the resource endpoint which provides access to protected resources. It is to the endpoints you will feed requests and get back an almost complete response. This process is simplified for you using a decorator such as the django one described -later. +later (but it's applicable to all other web frameworks librairies). The main purpose of the endpoint in OAuthLib is to figure out which grant type or token to dispatch the request to. diff --git a/docs/oauth2/server.rst b/docs/oauth2/server.rst index 9d6b5022..9900e36d 100644 --- a/docs/oauth2/server.rst +++ b/docs/oauth2/server.rst @@ -6,8 +6,10 @@ OAuthLib is a dependency free library that may be used with any web framework. That said, there are framework specific helper libraries to make your life easier. -- For Django there is `django-oauth-toolkit`_. -- For Flask there is `flask-oauthlib`_. +- Django `django-oauth-toolkit`_ +- Flask `flask-oauthlib`_ +- Pyramid `pyramid-oauthlib`_ +- Bottle `bottle-oauthlib`_ If there is no support for your favourite framework and you are interested in providing it then you have come to the right place. OAuthLib can handle @@ -17,6 +19,8 @@ as well as provide an interface for a backend to store tokens, clients, etc. .. _`django-oauth-toolkit`: https://github.com/evonove/django-oauth-toolkit .. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib +.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib +.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib .. contents:: Tutorial Contents :depth: 3 From d7fc1336d81b39f3d2193eb3155ff66da6caadd9 Mon Sep 17 00:00:00 2001 From: Antoine Bertin Date: Mon, 29 Jan 2018 10:17:54 +0100 Subject: [PATCH 015/237] Fix cliend_id in web request body (#505) Previously, cliend_id was always included in the request body in the Authorization Code flow and the client_id parameter was ignored in contradiction with the docs. Fixes #495 --- oauthlib/oauth2/rfc6749/clients/web_application.py | 2 +- tests/oauth2/rfc6749/clients/test_web_application.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index c099d999..bc62c8fb 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -125,7 +125,7 @@ def prepare_request_body(self, client_id=None, code=None, body='', """ code = code or self.code return prepare_token_request('authorization_code', code=code, body=body, - client_id=self.client_id, redirect_uri=redirect_uri, **kwargs) + client_id=client_id, redirect_uri=redirect_uri, **kwargs) def parse_request_uri_response(self, uri, state=None): """Parse the URI query for code and state. diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 85b247d1..0a80c9ad 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -38,7 +38,7 @@ class WebApplicationClientTest(TestCase): code = "zzzzaaaa" body = "not=empty" - body_code = "not=empty&grant_type=authorization_code&code=%s&client_id=%s" % (code, client_id) + body_code = "not=empty&grant_type=authorization_code&code=%s" % code body_redirect = body_code + "&redirect_uri=http%3A%2F%2Fmy.page.com%2Fcallback" body_kwargs = body_code + "&some=providers&require=extra+arguments" From 2fe1cdb88e076f624824496c4aba6a8665e991d9 Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Tue, 30 Jan 2018 17:30:26 -0200 Subject: [PATCH 016/237] Openid connect jwt (#488) * Add JWT token with it the server knows how to validate this new type of token in resource requests * Change find_token_type sorted function to reverse result and choose the valued estimated token handler * Add validate_id_token method to RequestValidator * Added unittest for JWTToken model * Updated version of Mock * Add get_jwt_bearer_token and validate_jwt_bearer_token oauthlib.oauth2.RequestValidator and change oauthlib.oauth2.tokens JWTToken to use it * Change to improve token type estimate test * Add a note in RequestValidator.validate_jwt_bearer_token about error 5xx rather 4xx --- .../rfc6749/endpoints/pre_configured.py | 7 +- oauthlib/oauth2/rfc6749/endpoints/resource.py | 2 +- oauthlib/oauth2/rfc6749/request_validator.py | 64 ++++++++- oauthlib/oauth2/rfc6749/tokens.py | 46 ++++++- requirements-test.txt | 2 +- setup.py | 2 +- tests/oauth2/rfc6749/test_tokens.py | 128 ++++++++++++++++++ 7 files changed, 243 insertions(+), 8 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index 07c37158..0c26986e 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -16,7 +16,7 @@ OpenIDConnectHybrid, RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant) -from ..tokens import BearerToken +from ..tokens import BearerToken, JWTToken from .authorization import AuthorizationEndpoint from .resource import ResourceEndpoint from .revocation import RevocationEndpoint @@ -57,6 +57,9 @@ def __init__(self, request_validator, token_expires_in=None, bearer = BearerToken(request_validator, token_generator, token_expires_in, refresh_token_generator) + jwt = JWTToken(request_validator, token_generator, + token_expires_in, refresh_token_generator) + auth_grant_choice = AuthCodeGrantDispatcher(default_auth_grant=auth_grant, oidc_auth_grant=openid_connect_auth) implicit_grant_choice = ImplicitTokenGrantDispatcher(default_implicit_grant=implicit_grant, oidc_implicit_grant=openid_connect_implicit) @@ -86,7 +89,7 @@ def __init__(self, request_validator, token_expires_in=None, }, default_token_type=bearer) ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': bearer}) + token_types={'Bearer': bearer, 'JWT': jwt}) RevocationEndpoint.__init__(self, request_validator) diff --git a/oauthlib/oauth2/rfc6749/endpoints/resource.py b/oauthlib/oauth2/rfc6749/endpoints/resource.py index d03ed214..f19c60c3 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/resource.py +++ b/oauthlib/oauth2/rfc6749/endpoints/resource.py @@ -83,5 +83,5 @@ def find_token_type(self, request): to give an estimation based on the request. """ estimates = sorted(((t.estimate_type(request), n) - for n, t in self.tokens.items())) + for n, t in self.tokens.items()), reverse=True) return estimates[0][1] if len(estimates) else None diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index ba129d5e..d25a6e05 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -312,8 +312,24 @@ def save_bearer_token(self, token, request, *args, **kwargs): """ raise NotImplementedError('Subclasses must implement this method.') - def get_id_token(self, token, token_handler, request): + def get_jwt_bearer_token(self, token, token_handler, request): + """Get JWT Bearer token or OpenID Connect ID token + + If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token` + + :param token: A Bearer token dict + :param token_handler: the token handler (BearerToken class) + :param request: the HTTP Request (oauthlib.common.Request) + :return: The JWT Bearer token or OpenID Connect ID token (a JWS signed JWT) + + Method is used by JWT Bearer and OpenID Connect tokens: + - JWTToken.create_token """ + raise NotImplementedError('Subclasses must implement this method.') + + def get_id_token(self, token, token_handler, request): + """Get OpenID Connect ID token + In the OpenID Connect workflows when an ID Token is requested this method is called. Subclasses should implement the construction, signing and optional encryption of the ID Token as described in the OpenID Connect spec. @@ -344,6 +360,52 @@ def get_id_token(self, token, token_handler, request): # the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token raise NotImplementedError('Subclasses must implement this method.') + def validate_jwt_bearer_token(self, token, scopes, request): + """Ensure the JWT Bearer token or OpenID Connect ID token are valids and authorized access to scopes. + + If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token` + + If not using OpenID Connect this can `return None` to avoid 5xx rather 401/3 response. + + OpenID connect core 1.0 describe how to validate an id_token: + - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2 + + :param token: Unicode Bearer token + :param scopes: List of scopes (defined by you) + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is indirectly used by all core OpenID connect JWT token issuing grant types: + - Authorization Code Grant + - Implicit Grant + - Hybrid Grant + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_id_token(self, token, scopes, request): + """Ensure the id token is valid and authorized access to scopes. + + OpenID connect core 1.0 describe how to validate an id_token: + - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2 + + :param token: Unicode Bearer token + :param scopes: List of scopes (defined by you) + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is indirectly used by all core OpenID connect JWT token issuing grant types: + - Authorization Code Grant + - Implicit Grant + - Hybrid Grant + """ + raise NotImplementedError('Subclasses must implement this method.') + def validate_bearer_token(self, token, scopes, request): """Ensure the Bearer token is valid and authorized access to scopes. diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index e0ac4318..e68ba597 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -24,8 +24,6 @@ from urllib.parse import urlparse - - class OAuth2Token(dict): def __init__(self, params, old_scope=None): @@ -303,3 +301,47 @@ def estimate_type(self, request): return 5 else: return 0 + + +class JWTToken(TokenBase): + __slots__ = ( + 'request_validator', 'token_generator', + 'refresh_token_generator', 'expires_in' + ) + + def __init__(self, request_validator=None, token_generator=None, + expires_in=None, refresh_token_generator=None): + self.request_validator = request_validator + self.token_generator = token_generator or random_token_generator + self.refresh_token_generator = ( + refresh_token_generator or self.token_generator + ) + self.expires_in = expires_in or 3600 + + def create_token(self, request, refresh_token=False, save_token=False): + """Create a JWT Token, using requestvalidator method.""" + + if callable(self.expires_in): + expires_in = self.expires_in(request) + else: + expires_in = self.expires_in + + request.expires_in = expires_in + + return self.request_validator.get_jwt_bearer_token(None, None, request) + + def validate_request(self, request): + token = None + if 'Authorization' in request.headers: + token = request.headers.get('Authorization')[7:] + else: + token = request.access_token + return self.request_validator.validate_jwt_bearer_token( + token, request.scopes, request) + + def estimate_type(self, request): + token = request.headers.get('Authorization', '')[7:] + if token.startswith('ey') and token.count('.') in (2, 4): + return 10 + else: + return 0 diff --git a/requirements-test.txt b/requirements-test.txt index e761883e..5bf6e065 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -r requirements.txt coverage>=3.7.1 nose==1.3.7 -mock==1.0.1 +mock>=2.0 diff --git a/setup.py b/setup.py index 43f4d959..4640ec80 100755 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def fread(fn): if sys.version_info[0] == 3: tests_require = ['nose', 'cryptography', 'pyjwt>=1.0.0', 'blinker'] else: - tests_require = ['nose', 'unittest2', 'cryptography', 'mock', 'pyjwt>=1.0.0', 'blinker'] + tests_require = ['nose', 'unittest2', 'cryptography', 'mock>=2.0', 'pyjwt>=1.0.0', 'blinker'] rsa_require = ['cryptography'] signedtoken_require = ['cryptography', 'pyjwt>=1.0.0'] signals_require = ['blinker'] diff --git a/tests/oauth2/rfc6749/test_tokens.py b/tests/oauth2/rfc6749/test_tokens.py index e2e558d9..570afb01 100644 --- a/tests/oauth2/rfc6749/test_tokens.py +++ b/tests/oauth2/rfc6749/test_tokens.py @@ -1,5 +1,7 @@ from __future__ import absolute_import, unicode_literals +import mock + from oauthlib.oauth2.rfc6749.tokens import * from ...unittest import TestCase @@ -80,3 +82,129 @@ def test_prepare_bearer_request(self): self.assertEqual(prepare_bearer_headers(self.token), self.bearer_headers) self.assertEqual(prepare_bearer_body(self.token), self.bearer_body) self.assertEqual(prepare_bearer_uri(self.token, uri=self.uri), self.bearer_uri) + + +class JWTTokenTestCase(TestCase): + + def test_create_token_callable_expires_in(self): + """ + Test retrieval of the expires in value by calling the callable expires_in property + """ + + expires_in_mock = mock.MagicMock() + request_mock = mock.MagicMock() + + token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock()) + token.create_token(request=request_mock) + + expires_in_mock.assert_called_once_with(request_mock) + + def test_create_token_non_callable_expires_in(self): + """ + When a non callable expires in is set this should just be set to the request + """ + + expires_in_mock = mock.NonCallableMagicMock() + request_mock = mock.MagicMock() + + token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock()) + token.create_token(request=request_mock) + + self.assertFalse(expires_in_mock.called) + self.assertEqual(request_mock.expires_in, expires_in_mock) + + def test_create_token_calls_get_id_token(self): + """ + When create_token is called the call should be forwarded to the get_id_token on the token validator + """ + request_mock = mock.MagicMock() + + with mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + autospec=True) as RequestValidatorMock: + + request_validator = RequestValidatorMock() + + token = JWTToken(expires_in=mock.MagicMock(), request_validator=request_validator) + token.create_token(request=request_mock) + + request_validator.get_jwt_bearer_token.assert_called_once_with(None, None, request_mock) + + def test_validate_request_token_from_headers(self): + """ + Bearer token get retrieved from headers. + """ + + with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \ + mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + autospec=True) as RequestValidatorMock: + request_validator_mock = RequestValidatorMock() + + token = JWTToken(request_validator=request_validator_mock) + + request = RequestMock('/uri') + # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch + # with autospec=True + request.scopes = mock.MagicMock() + request.headers = { + 'Authorization': 'Bearer some-token-from-header' + } + + token.validate_request(request=request) + + request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-header', + request.scopes, + request) + + def test_validate_token_from_request(self): + """ + Token get retrieved from request object. + """ + + with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \ + mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + autospec=True) as RequestValidatorMock: + request_validator_mock = RequestValidatorMock() + + token = JWTToken(request_validator=request_validator_mock) + + request = RequestMock('/uri') + # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch + # with autospec=True + request.scopes = mock.MagicMock() + request.access_token = 'some-token-from-request-object' + request.headers = {} + + token.validate_request(request=request) + + request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-request-object', + request.scopes, + request) + + def test_estimate_type(self): + """ + Estimate type results for a jwt token + """ + + def test_token(token, expected_result): + with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock: + jwt_token = JWTToken() + + request = RequestMock('/uri') + # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch + # with autospec=True + request.headers = { + 'Authorization': 'Bearer {}'.format(token) + } + + result = jwt_token.estimate_type(request=request) + + self.assertEqual(result, expected_result) + + test_items = ( + ('eyfoo.foo.foo', 10), + ('eyfoo.foo.foo.foo.foo', 10), + ('eyfoobar', 0) + ) + + for token, expected_result in test_items: + test_token(token, expected_result) From 32e5ad1509a8d46fa402776f54fbabef4b1ded63 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 28 Feb 2018 15:00:08 +0100 Subject: [PATCH 017/237] Rtd docs fix (#515) * Added sphinx build for developers Rationale is to build docs locally to prevent RTD to break later. * Replace manual sphinx into make * Renamed idan URL to oauthlib community * Renamed http into https URLs since http is returning 302 * python requests library renamed its home URL * Add ignore list for "make linkcheck" linkcheck is doing requests to github with anonymous access, however creating an issue require an logged-in account * virtualenv changed its homepage and website. * Fixed broken link --- README.rst | 14 +-- docs/conf.py | 2 + docs/contributing.rst | 10 +-- docs/faq.rst | 4 +- docs/index.rst | 2 +- docs/installation.rst | 2 +- docs/oauth1/preconfigured_servers.rst | 2 +- docs/oauth1/server.rst | 2 +- docs/oauth2/clients/client.rst | 2 +- docs/oauth2/grants/jwt.rst | 2 +- docs/oauth2/server.rst | 4 +- docs/oauth2/tokens/mac.rst | 2 +- docs/oauth2/tokens/saml.rst | 2 +- docs/oauth2/tokens/tokens.rst | 4 +- oauthlib/common.py | 10 +-- oauthlib/oauth1/rfc5849/__init__.py | 4 +- .../oauth1/rfc5849/endpoints/access_token.py | 2 +- oauthlib/oauth1/rfc5849/endpoints/base.py | 6 +- .../oauth1/rfc5849/endpoints/request_token.py | 4 +- oauthlib/oauth1/rfc5849/endpoints/resource.py | 2 +- oauthlib/oauth1/rfc5849/parameters.py | 20 ++--- oauthlib/oauth1/rfc5849/request_validator.py | 6 +- oauthlib/oauth1/rfc5849/signature.py | 86 +++++++++---------- oauthlib/oauth1/rfc5849/utils.py | 2 +- .../rfc6749/clients/backend_application.py | 6 +- oauthlib/oauth2/rfc6749/clients/base.py | 10 +-- .../rfc6749/clients/legacy_application.py | 6 +- .../rfc6749/clients/mobile_application.py | 14 +-- .../rfc6749/clients/service_application.py | 2 +- .../oauth2/rfc6749/clients/web_application.py | 14 +-- .../oauth2/rfc6749/endpoints/authorization.py | 2 +- .../oauth2/rfc6749/endpoints/revocation.py | 12 +-- oauthlib/oauth2/rfc6749/endpoints/token.py | 2 +- .../rfc6749/grant_types/authorization_code.py | 26 +++--- .../rfc6749/grant_types/client_credentials.py | 6 +- .../oauth2/rfc6749/grant_types/implicit.py | 34 ++++---- .../rfc6749/grant_types/refresh_token.py | 8 +- .../resource_owner_password_credentials.py | 10 +-- oauthlib/oauth2/rfc6749/parameters.py | 32 +++---- oauthlib/oauth2/rfc6749/request_validator.py | 8 +- oauthlib/oauth2/rfc6749/tokens.py | 14 +-- setup.py | 2 +- tox.ini | 8 +- 43 files changed, 210 insertions(+), 202 deletions(-) diff --git a/README.rst b/README.rst index 656d72c3..b4892a80 100644 --- a/README.rst +++ b/README.rst @@ -4,10 +4,10 @@ OAuthLib *A generic, spec-compliant, thorough implementation of the OAuth request-signing logic for python* -.. image:: https://travis-ci.org/idan/oauthlib.svg?branch=master - :target: https://travis-ci.org/idan/oauthlib -.. image:: https://coveralls.io/repos/idan/oauthlib/badge.svg?branch=master - :target: https://coveralls.io/r/idan/oauthlib +.. image:: https://travis-ci.org/oauthlib/oauthlib.svg?branch=master + :target: https://travis-ci.org/oauthlib/oauthlib +.. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master + :target: https://coveralls.io/r/oauthlib/oauthlib OAuth often seems complicated and difficult-to-implement. There are several @@ -18,8 +18,8 @@ both of the following: 2. They predate the `OAuth 2.0 spec`_, AKA RFC 6749. 3. They assume the usage of a specific HTTP request library. -.. _`OAuth 1.0 spec`: http://tools.ietf.org/html/rfc5849 -.. _`OAuth 2.0 spec`: http://tools.ietf.org/html/rfc6749 +.. _`OAuth 1.0 spec`: https://tools.ietf.org/html/rfc5849 +.. _`OAuth 2.0 spec`: https://tools.ietf.org/html/rfc6749 OAuthLib is a generic utility which implements the logic of OAuth without assuming a specific HTTP request object or web framework. Use it to graft OAuth @@ -45,7 +45,7 @@ Interested in making OAuth requests? Then you might be more interested in using `requests`_ which has OAuthLib powered OAuth support provided by the `requests-oauthlib`_ library. -.. _`requests`: https://github.com/kennethreitz/requests +.. _`requests`: https://github.com/requests/requests .. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib Which web frameworks are supported? diff --git a/docs/conf.py b/docs/conf.py index fb14d051..b1ca34d9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -243,3 +243,5 @@ # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' + +linkcheck_ignore = ["https://github.com/oauthlib/oauthlib/issues/new"] diff --git a/docs/contributing.rst b/docs/contributing.rst index f3de44d6..601c5670 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -91,7 +91,7 @@ request only to have it rejected because it has diverged too far from master. To pull in upstream changes:: - git remote add upstream https://github.com/idan/oauthlib.git + git remote add upstream https://github.com/oauthlib/oauthlib.git git fetch upstream Check the log to be sure that you actually want the changes, before merging:: @@ -102,7 +102,7 @@ Then merge the changes that you fetched:: git merge upstream/master -For more info, see http://help.github.com/fork-a-repo/ +For more info, see https://help.github.com/fork-a-repo/ How to get your pull request accepted ===================================== @@ -148,7 +148,7 @@ version. For Ubuntu you can easily install all after adding one ppa. $ sudo apt-get install pypy pypy-dev .. _`Tox`: https://tox.readthedocs.io/en/latest/install.html -.. _`virtualenv`: http://www.virtualenv.org/en/latest/#installation +.. _`virtualenv`: https://virtualenv.pypa.io/en/latest/installation/ If you add code you need to add tests! -------------------------------------- @@ -223,5 +223,5 @@ to GitHub:: git push upstream master .. _installation: install.html -.. _GitHub project: https://github.com/idan/oauthlib -.. _issue tracker: https://github.com/idan/oauthlib/issues +.. _GitHub project: https://github.com/oauthlib/oauthlib +.. _issue tracker: https://github.com/oauthlib/oauthlib/issues diff --git a/docs/faq.rst b/docs/faq.rst index 0c61af9d..38b0e92a 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -100,6 +100,6 @@ Some argue OAuth 2 is worse than 1, is that true? .. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib .. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib .. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib -.. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new +.. _`GitHub issue`: https://github.com/oauthlib/oauthlib/issues/new .. _`G+`: https://plus.google.com/communities/101889017375384052571 -.. _`difference`: http://www.cyberciti.biz/faq/authentication-vs-authorization/ +.. _`difference`: https://www.cyberciti.biz/faq/authentication-vs-authorization/ diff --git a/docs/index.rst b/docs/index.rst index 16990687..1da2ca57 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,7 +13,7 @@ Check out :doc:`error_reporting` for details on how to be an awesome bug reporte For news and discussions please head over to our `G+ OAuthLib community`_. -.. _`new issue on GitHub`: https://github.com/idan/oauthlib/issues/new +.. _`new issue on GitHub`: https://github.com/oauthlib/oauthlib/issues/new .. _`G+ OAuthLib community`: https://plus.google.com/communities/101889017375384052571 .. toctree:: diff --git a/docs/installation.rst b/docs/installation.rst index 5a8b2cbe..48e42887 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -22,7 +22,7 @@ Bleeding edge from GitHub master .. code-block:: bash - pip install -e git+https://github.com/idan/oauthlib.git#egg=oauthlib + pip install -e git+https://github.com/oauthlib/oauthlib.git#egg=oauthlib Debian and derivatives like Ubuntu, Mint, etc. --------------------------------------------- diff --git a/docs/oauth1/preconfigured_servers.rst b/docs/oauth1/preconfigured_servers.rst index 7f7f3866..b32e1abc 100644 --- a/docs/oauth1/preconfigured_servers.rst +++ b/docs/oauth1/preconfigured_servers.rst @@ -12,7 +12,7 @@ Construction is simple, only import your validator and you are good to go:: server = WebApplicationServer(your_validator) -All endpoints are documented in :doc:`endpoints`. +All endpoints are documented in :doc:`Provider endpoints `. .. autoclass:: oauthlib.oauth1.WebApplicationServer :members: diff --git a/docs/oauth1/server.rst b/docs/oauth1/server.rst index f254c91e..2a91f302 100644 --- a/docs/oauth1/server.rst +++ b/docs/oauth1/server.rst @@ -436,7 +436,7 @@ shown below as well as run your flask server locally on port `5000`. Drop a line in our `G+ community`_ or open a `GitHub issue`_ =) .. _`G+ community`: https://plus.google.com/communities/101889017375384052571 -.. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new +.. _`GitHub issue`: https://github.com/oauthlib/oauthlib/issues/new If you run into issues it can be helpful to enable debug logging:: diff --git a/docs/oauth2/clients/client.rst b/docs/oauth2/clients/client.rst index 11da2cc4..9a5a4ffe 100644 --- a/docs/oauth2/clients/client.rst +++ b/docs/oauth2/clients/client.rst @@ -24,5 +24,5 @@ to use them please browse the documentation for each client type below. If you are interested in integrating OAuth 2 support into your favourite HTTP library you might find the requests-oauthlib implementation interesting. - .. _`requests`: https://github.com/kennethreitz/requests + .. _`requests`: https://github.com/requests/requests .. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib diff --git a/docs/oauth2/grants/jwt.rst b/docs/oauth2/grants/jwt.rst index 87aed11c..db653424 100644 --- a/docs/oauth2/grants/jwt.rst +++ b/docs/oauth2/grants/jwt.rst @@ -4,4 +4,4 @@ JWT Tokens Not yet implemented. Track progress in `GitHub issue 50`_. -.. _`GitHub issue 50`: https://github.com/idan/oauthlib/issues/50 +.. _`GitHub issue 50`: https://github.com/oauthlib/oauthlib/issues/50 diff --git a/docs/oauth2/server.rst b/docs/oauth2/server.rst index 9900e36d..8f8b77bc 100644 --- a/docs/oauth2/server.rst +++ b/docs/oauth2/server.rst @@ -279,7 +279,7 @@ all methods depending on which grant types you wish to support. A skeleton validator listing the methods required for the WebApplicationServer is available in the `examples`_ folder on GitHub. -.. _`examples`: https://github.com/idan/oauthlib/blob/master/examples/skeleton_oauth2_web_application_server.py +.. _`examples`: https://github.com/oauthlib/oauthlib/blob/master/examples/skeleton_oauth2_web_application_server.py Relevant sections include: @@ -496,7 +496,7 @@ at runtime by a function, rather then by a list. Drop a line in our `G+ community`_ or open a `GitHub issue`_ =) .. _`G+ community`: https://plus.google.com/communities/101889017375384052571 -.. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new +.. _`GitHub issue`: https://github.com/oauthlib/oauthlib/issues/new If you run into issues it can be helpful to enable debug logging. diff --git a/docs/oauth2/tokens/mac.rst b/docs/oauth2/tokens/mac.rst index 49868198..afb69485 100644 --- a/docs/oauth2/tokens/mac.rst +++ b/docs/oauth2/tokens/mac.rst @@ -5,4 +5,4 @@ MAC tokens Not yet implemented. Track progress in `GitHub issue 29`_. Might never be supported depending on whether the work on the specification is resumed or not. -.. _`GitHub issue 29`: https://github.com/idan/oauthlib/issues/29 +.. _`GitHub issue 29`: https://github.com/oauthlib/oauthlib/issues/29 diff --git a/docs/oauth2/tokens/saml.rst b/docs/oauth2/tokens/saml.rst index 9a009370..5faf16ae 100644 --- a/docs/oauth2/tokens/saml.rst +++ b/docs/oauth2/tokens/saml.rst @@ -4,4 +4,4 @@ SAML Tokens Not yet implemented. Track progress in `GitHub issue 49`_. -.. _`GitHub issue 49`: https://github.com/idan/oauthlib/issues/49 +.. _`GitHub issue 49`: https://github.com/oauthlib/oauthlib/issues/49 diff --git a/docs/oauth2/tokens/tokens.rst b/docs/oauth2/tokens/tokens.rst index f0adc97f..f3415097 100644 --- a/docs/oauth2/tokens/tokens.rst +++ b/docs/oauth2/tokens/tokens.rst @@ -15,8 +15,8 @@ providers, notably Facebook, do not provide this information. Per the is missing. You can force a ``MissingTokenTypeError`` exception instead, by setting ``OAUTHLIB_STRICT_TOKEN_TYPE`` in the environment. -.. _requires: http://tools.ietf.org/html/rfc6749#section-5.1 -.. _robustness principle: http://en.wikipedia.org/wiki/Robustness_principle +.. _requires: https://tools.ietf.org/html/rfc6749#section-5.1 +.. _robustness principle: https://en.wikipedia.org/wiki/Robustness_principle .. toctree:: :maxdepth: 2 diff --git a/oauthlib/common.py b/oauthlib/common.py index 705cbd25..afcc09ca 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -199,8 +199,8 @@ def generate_nonce(): A random 64-bit number is appended to the epoch timestamp for both randomness and to decrease the likelihood of collisions. - .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1 - .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3 + .. _`section 3.2.1`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1 + .. _`section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3 """ return unicode_type(unicode_type(random.getrandbits(64)) + generate_timestamp()) @@ -211,8 +211,8 @@ def generate_timestamp(): Per `section 3.3`_ of the OAuth 1 RFC 5849 spec. Per `section 3.2.1`_ of the MAC Access Authentication spec. - .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1 - .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3 + .. _`section 3.2.1`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1 + .. _`section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3 """ return unicode_type(int(time.time())) @@ -257,7 +257,7 @@ def generate_client_id(length=30, chars=CLIENT_ID_CHARACTER_SET): """Generates an OAuth client_id OAuth 2 specify the format of client_id in - http://tools.ietf.org/html/rfc6749#appendix-A. + https://tools.ietf.org/html/rfc6749#appendix-A. """ return generate_token(length, chars) diff --git a/oauthlib/oauth1/rfc5849/__init__.py b/oauthlib/oauth1/rfc5849/__init__.py index f9113ab5..87a8e6ba 100644 --- a/oauthlib/oauth1/rfc5849/__init__.py +++ b/oauthlib/oauth1/rfc5849/__init__.py @@ -122,7 +122,7 @@ def get_oauth_signature(self, request): replace any netloc part of the request argument's uri attribute value. - .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2 + .. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2 """ if self.signature_method == SIGNATURE_PLAINTEXT: # fast-path @@ -300,7 +300,7 @@ def sign(self, uri, http_method='GET', body=None, headers=None, realm=None): raise ValueError( 'Body signatures may only be used with form-urlencoded content') - # We amend http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1 + # We amend https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1 # with the clause that parameters from body should only be included # in non GET or HEAD requests. Extracting the request body parameters # and including them in the signature base string would give semantic diff --git a/oauthlib/oauth1/rfc5849/endpoints/access_token.py b/oauthlib/oauth1/rfc5849/endpoints/access_token.py index 12b901cb..12d13e9b 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/access_token.py +++ b/oauthlib/oauth1/rfc5849/endpoints/access_token.py @@ -180,7 +180,7 @@ def validate_access_token_request(self, request): # token credentials to the client, and ensure that the temporary # credentials have not expired or been used before. The server MUST # also verify the verification code received from the client. - # .. _`Section 3.2`: http://tools.ietf.org/html/rfc5849#section-3.2 + # .. _`Section 3.2`: https://tools.ietf.org/html/rfc5849#section-3.2 # # Note that early exit would enable resource owner authorization # verifier enumertion. diff --git a/oauthlib/oauth1/rfc5849/endpoints/base.py b/oauthlib/oauth1/rfc5849/endpoints/base.py index 9d51e69b..9702939b 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/base.py +++ b/oauthlib/oauth1/rfc5849/endpoints/base.py @@ -127,7 +127,7 @@ def _check_mandatory_parameters(self, request): # specification. Implementers should review the Security # Considerations section (`Section 4`_) before deciding on which # method to support. - # .. _`Section 4`: http://tools.ietf.org/html/rfc5849#section-4 + # .. _`Section 4`: https://tools.ietf.org/html/rfc5849#section-4 if (not request.signature_method in self.request_validator.allowed_signature_methods): raise errors.InvalidSignatureMethodError( @@ -181,7 +181,7 @@ def _check_signature(self, request, is_token_request=False): # ---- RSA Signature verification ---- if request.signature_method == SIGNATURE_RSA: # The server verifies the signature per `[RFC3447] section 8.2.2`_ - # .. _`[RFC3447] section 8.2.2`: http://tools.ietf.org/html/rfc3447#section-8.2.1 + # .. _`[RFC3447] section 8.2.2`: https://tools.ietf.org/html/rfc3447#section-8.2.1 rsa_key = self.request_validator.get_rsa_key( request.client_key, request) valid_signature = signature.verify_rsa_sha1(request, rsa_key) @@ -192,7 +192,7 @@ def _check_signature(self, request, is_token_request=False): # Recalculating the request signature independently as described in # `Section 3.4`_ and comparing it to the value received from the # client via the "oauth_signature" parameter. - # .. _`Section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4 + # .. _`Section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4 client_secret = self.request_validator.get_client_secret( request.client_key, request) resource_owner_secret = None diff --git a/oauthlib/oauth1/rfc5849/endpoints/request_token.py b/oauthlib/oauth1/rfc5849/endpoints/request_token.py index 515395b9..88fd6c09 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/request_token.py +++ b/oauthlib/oauth1/rfc5849/endpoints/request_token.py @@ -156,7 +156,7 @@ def validate_request_token_request(self, request): # However they could be seen as a scope or realm to which the # client has access and as such every client should be checked # to ensure it is authorized access to that scope or realm. - # .. _`realm`: http://tools.ietf.org/html/rfc2617#section-1.2 + # .. _`realm`: https://tools.ietf.org/html/rfc2617#section-1.2 # # Note that early exit would enable client realm access enumeration. # @@ -178,7 +178,7 @@ def validate_request_token_request(self, request): # Callback is normally never required, except for requests for # a Temporary Credential as described in `Section 2.1`_ - # .._`Section 2.1`: http://tools.ietf.org/html/rfc5849#section-2.1 + # .._`Section 2.1`: https://tools.ietf.org/html/rfc5849#section-2.1 valid_redirect = self.request_validator.validate_redirect_uri( request.client_key, request.redirect_uri, request) if not request.redirect_uri: diff --git a/oauthlib/oauth1/rfc5849/endpoints/resource.py b/oauthlib/oauth1/rfc5849/endpoints/resource.py index 53f95626..f82e8b1b 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/resource.py +++ b/oauthlib/oauth1/rfc5849/endpoints/resource.py @@ -119,7 +119,7 @@ def validate_protected_resource_request(self, uri, http_method='GET', # However they could be seen as a scope or realm to which the # client has access and as such every client should be checked # to ensure it is authorized access to that scope or realm. - # .. _`realm`: http://tools.ietf.org/html/rfc2617#section-1.2 + # .. _`realm`: https://tools.ietf.org/html/rfc2617#section-1.2 # # Note that early exit would enable client realm access enumeration. # diff --git a/oauthlib/oauth1/rfc5849/parameters.py b/oauthlib/oauth1/rfc5849/parameters.py index dcb23dc4..2f068a7c 100644 --- a/oauthlib/oauth1/rfc5849/parameters.py +++ b/oauthlib/oauth1/rfc5849/parameters.py @@ -5,7 +5,7 @@ This module contains methods related to `section 3.5`_ of the OAuth 1.0a spec. -.. _`section 3.5`: http://tools.ietf.org/html/rfc5849#section-3.5 +.. _`section 3.5`: https://tools.ietf.org/html/rfc5849#section-3.5 """ from __future__ import absolute_import, unicode_literals @@ -42,8 +42,8 @@ def prepare_headers(oauth_params, headers=None, realm=None): oauth_version="1.0" - .. _`section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1 - .. _`RFC2617`: http://tools.ietf.org/html/rfc2617 + .. _`section 3.5.1`: https://tools.ietf.org/html/rfc5849#section-3.5.1 + .. _`RFC2617`: https://tools.ietf.org/html/rfc2617 """ headers = headers or {} @@ -54,7 +54,7 @@ def prepare_headers(oauth_params, headers=None, realm=None): # 1. Parameter names and values are encoded per Parameter Encoding # (`Section 3.6`_) # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 escaped_name = utils.escape(oauth_parameter_name) escaped_value = utils.escape(value) @@ -68,14 +68,14 @@ def prepare_headers(oauth_params, headers=None, realm=None): # 3. Parameters are separated by a "," character (ASCII code 44) and # OPTIONAL linear whitespace per `RFC2617`_. # - # .. _`RFC2617`: http://tools.ietf.org/html/rfc2617 + # .. _`RFC2617`: https://tools.ietf.org/html/rfc2617 authorization_header_parameters = ', '.join( authorization_header_parameters_parts) # 4. The OPTIONAL "realm" parameter MAY be added and interpreted per # `RFC2617 section 1.2`_. # - # .. _`RFC2617 section 1.2`: http://tools.ietf.org/html/rfc2617#section-1.2 + # .. _`RFC2617 section 1.2`: https://tools.ietf.org/html/rfc2617#section-1.2 if realm: # NOTE: realm should *not* be escaped authorization_header_parameters = ('realm="%s", ' % realm + @@ -98,8 +98,8 @@ def _append_params(oauth_params, params): Per `section 3.5.2`_ and `3.5.3`_ of the spec. - .. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2 - .. _`3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3 + .. _`section 3.5.2`: https://tools.ietf.org/html/rfc5849#section-3.5.2 + .. _`3.5.3`: https://tools.ietf.org/html/rfc5849#section-3.5.3 """ merged = list(params) @@ -117,7 +117,7 @@ def prepare_form_encoded_body(oauth_params, body): Per `section 3.5.2`_ of the spec. - .. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2 + .. _`section 3.5.2`: https://tools.ietf.org/html/rfc5849#section-3.5.2 """ # append OAuth params to the existing body @@ -129,7 +129,7 @@ def prepare_request_uri_query(oauth_params, uri): Per `section 3.5.3`_ of the spec. - .. _`section 3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3 + .. _`section 3.5.3`: https://tools.ietf.org/html/rfc5849#section-3.5.3 """ # append OAuth params to the existing set of query components diff --git a/oauthlib/oauth1/rfc5849/request_validator.py b/oauthlib/oauth1/rfc5849/request_validator.py index 2ccb3672..bc62ea04 100644 --- a/oauthlib/oauth1/rfc5849/request_validator.py +++ b/oauthlib/oauth1/rfc5849/request_validator.py @@ -109,7 +109,7 @@ class RequestValidator(object): their use more straightforward and as such it could be worth reading what follows in chronological order. - .. _`whitelisting or blacklisting`: http://www.schneier.com/blog/archives/2011/01/whitelisting_vs.html + .. _`whitelisting or blacklisting`: https://www.schneier.com/blog/archives/2011/01/whitelisting_vs.html """ def __init__(self): @@ -445,7 +445,7 @@ def invalidate_request_token(self, client_key, request_token, request): "The server MUST (...) ensure that the temporary credentials have not expired or been used before." - .. _`Section 2.3`: http://tools.ietf.org/html/rfc5849#section-2.3 + .. _`Section 2.3`: https://tools.ietf.org/html/rfc5849#section-2.3 This method should ensure that provided token won't validate anymore. It can be simply removing RequestToken from storage or setting @@ -582,7 +582,7 @@ def validate_timestamp_and_nonce(self, client_key, timestamp, nonce, channel. The nonce value MUST be unique across all requests with the same timestamp, client credentials, and token combinations." - .. _`Section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3 One of the first validation checks that will be made is for the validity of the nonce and timestamp, which are associated with a client key and diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index 30001efd..4e672ba3 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -19,7 +19,7 @@ construct the base string 5. Pass the base string and any keys needed to a signing function -.. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4 +.. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4 """ from __future__ import absolute_import, unicode_literals @@ -69,7 +69,7 @@ def construct_base_string(http_method, base_string_uri, ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk 9d7dh3k39sjv7 - .. _`section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1 + .. _`section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1 """ # The signature base string is constructed by concatenating together, @@ -79,7 +79,7 @@ def construct_base_string(http_method, base_string_uri, # "GET", "POST", etc. If the request uses a custom HTTP method, it # MUST be encoded (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 base_string = utils.escape(http_method.upper()) # 2. An "&" character (ASCII code 38). @@ -88,8 +88,8 @@ def construct_base_string(http_method, base_string_uri, # 3. The base string URI from `Section 3.4.1.2`_, after being encoded # (`Section 3.6`_). # - # .. _`Section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2 - # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6 + # .. _`Section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2 + # .. _`Section 3.4.6`: https://tools.ietf.org/html/rfc5849#section-3.4.6 base_string += utils.escape(base_string_uri) # 4. An "&" character (ASCII code 38). @@ -98,8 +98,8 @@ def construct_base_string(http_method, base_string_uri, # 5. The request parameters as normalized in `Section 3.4.1.3.2`_, after # being encoded (`Section 3.6`). # - # .. _`Section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 - # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6 + # .. _`Section 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 + # .. _`Section 3.4.6`: https://tools.ietf.org/html/rfc5849#section-3.4.6 base_string += utils.escape(normalized_encoded_request_parameters) return base_string @@ -123,7 +123,7 @@ def normalize_base_string_uri(uri, host=None): is represented by the base string URI: "https://www.example.net:8080/". - .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2 + .. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2 The host argument overrides the netloc part of the uri argument. """ @@ -137,7 +137,7 @@ def normalize_base_string_uri(uri, host=None): # are included by constructing an "http" or "https" URI representing # the request resource (without the query or fragment) as follows: # - # .. _`RFC3986`: http://tools.ietf.org/html/rfc3986 + # .. _`RFC3986`: https://tools.ietf.org/html/rfc3986 if not scheme or not netloc: raise ValueError('uri must include a scheme and netloc') @@ -147,7 +147,7 @@ def normalize_base_string_uri(uri, host=None): # Note that the absolute path cannot be empty; if none is present in # the original URI, it MUST be given as "/" (the server root). # - # .. _`RFC 2616 section 5.1.2`: http://tools.ietf.org/html/rfc2616#section-5.1.2 + # .. _`RFC 2616 section 5.1.2`: https://tools.ietf.org/html/rfc2616#section-5.1.2 if not path: path = '/' @@ -166,8 +166,8 @@ def normalize_base_string_uri(uri, host=None): # to port 80 or when making an HTTPS request `RFC2818`_ to port 443. # All other non-default port numbers MUST be included. # - # .. _`RFC2616`: http://tools.ietf.org/html/rfc2616 - # .. _`RFC2818`: http://tools.ietf.org/html/rfc2818 + # .. _`RFC2616`: https://tools.ietf.org/html/rfc2616 + # .. _`RFC2818`: https://tools.ietf.org/html/rfc2818 default_ports = ( ('http', '80'), ('https', '443'), @@ -190,7 +190,7 @@ def normalize_base_string_uri(uri, host=None): # particular manner that is often different from their original # encoding scheme, and concatenated into a single string. # -# .. _`section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3 +# .. _`section 3.4.1.3`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3 def collect_parameters(uri_query='', body=[], headers=None, exclude_oauth_signature=True, with_realm=False): @@ -249,7 +249,7 @@ def collect_parameters(uri_query='', body=[], headers=None, parameter instances (the "a3" parameter is used twice in this request). - .. _`section 3.4.1.3.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1 + .. _`section 3.4.1.3.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1 """ headers = headers or {} params = [] @@ -264,8 +264,8 @@ def collect_parameters(uri_query='', body=[], headers=None, # and values and decoding them as defined by # `W3C.REC-html40-19980424`_, Section 17.13.4. # - # .. _`RFC3986, Section 3.4`: http://tools.ietf.org/html/rfc3986#section-3.4 - # .. _`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424 + # .. _`RFC3986, Section 3.4`: https://tools.ietf.org/html/rfc3986#section-3.4 + # .. _`W3C.REC-html40-19980424`: https://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424 if uri_query: params.extend(urldecode(uri_query)) @@ -274,7 +274,7 @@ def collect_parameters(uri_query='', body=[], headers=None, # pairs excluding the "realm" parameter if present. The parameter # values are decoded as defined by `Section 3.5.1`_. # - # .. _`Section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1 + # .. _`Section 3.5.1`: https://tools.ietf.org/html/rfc5849#section-3.5.1 if headers: headers_lower = dict((k.lower(), v) for k, v in headers.items()) authorization_header = headers_lower.get('authorization') @@ -293,7 +293,7 @@ def collect_parameters(uri_query='', body=[], headers=None, # * The HTTP request entity-header includes the "Content-Type" # header field set to "application/x-www-form-urlencoded". # - # .._`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424 + # .._`W3C.REC-html40-19980424`: https://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424 # TODO: enforce header param inclusion conditions bodyparams = extract_params(body) or [] @@ -383,18 +383,18 @@ def normalize_parameters(params): dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1 &oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7 - .. _`section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 + .. _`section 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 """ # The parameters collected in `Section 3.4.1.3`_ are normalized into a # single string as follows: # - # .. _`Section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3 + # .. _`Section 3.4.1.3`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3 # 1. First, the name and value of each parameter are encoded # (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 key_values = [(utils.escape(k), utils.escape(v)) for k, v in params] # 2. The parameters are sorted by name, using ascending byte value @@ -430,8 +430,8 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret): Per `section 3.4.2`_ of the spec. - .. _`RFC2104`: http://tools.ietf.org/html/rfc2104 - .. _`section 3.4.2`: http://tools.ietf.org/html/rfc5849#section-3.4.2 + .. _`RFC2104`: https://tools.ietf.org/html/rfc2104 + .. _`section 3.4.2`: https://tools.ietf.org/html/rfc5849#section-3.4.2 """ # The HMAC-SHA1 function variables are used in following way: @@ -439,13 +439,13 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret): # text is set to the value of the signature base string from # `Section 3.4.1.1`_. # - # .. _`Section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1 + # .. _`Section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1 text = base_string # key is set to the concatenated values of: # 1. The client shared-secret, after being encoded (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 key = utils.escape(client_secret or '') # 2. An "&" character (ASCII code 38), which MUST be included @@ -454,7 +454,7 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret): # 3. The token shared-secret, after being encoded (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 key += utils.escape(resource_owner_secret or '') # FIXME: HMAC does not support unicode! @@ -466,7 +466,7 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret): # parameter, after the result octet string is base64-encoded # per `RFC2045, Section 6.8`. # - # .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8 + # .. _`RFC2045, Section 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8 return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8') @@ -487,8 +487,8 @@ def sign_hmac_sha256(base_string, client_secret, resource_owner_secret): Per `section 3.4.2`_ of the spec. - .. _`RFC4634`: http://tools.ietf.org/html/rfc4634 - .. _`section 3.4.2`: http://tools.ietf.org/html/rfc5849#section-3.4.2 + .. _`RFC4634`: https://tools.ietf.org/html/rfc4634 + .. _`section 3.4.2`: https://tools.ietf.org/html/rfc5849#section-3.4.2 """ # The HMAC-SHA256 function variables are used in following way: @@ -496,13 +496,13 @@ def sign_hmac_sha256(base_string, client_secret, resource_owner_secret): # text is set to the value of the signature base string from # `Section 3.4.1.1`_. # - # .. _`Section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1 + # .. _`Section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1 text = base_string # key is set to the concatenated values of: # 1. The client shared-secret, after being encoded (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 key = utils.escape(client_secret or '') # 2. An "&" character (ASCII code 38), which MUST be included @@ -511,7 +511,7 @@ def sign_hmac_sha256(base_string, client_secret, resource_owner_secret): # 3. The token shared-secret, after being encoded (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 key += utils.escape(resource_owner_secret or '') # FIXME: HMAC does not support unicode! @@ -523,7 +523,7 @@ def sign_hmac_sha256(base_string, client_secret, resource_owner_secret): # parameter, after the result octet string is base64-encoded # per `RFC2045, Section 6.8`. # - # .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8 + # .. _`RFC2045, Section 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8 return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8') _jwtrs1 = None @@ -548,8 +548,8 @@ def sign_rsa_sha1(base_string, rsa_private_key): with the server that included its RSA public key (in a manner that is beyond the scope of this specification). - .. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3 - .. _`RFC3447, Section 8.2`: http://tools.ietf.org/html/rfc3447#section-8.2 + .. _`section 3.4.3`: https://tools.ietf.org/html/rfc5849#section-3.4.3 + .. _`RFC3447, Section 8.2`: https://tools.ietf.org/html/rfc3447#section-8.2 """ if isinstance(base_string, unicode_type): @@ -578,7 +578,7 @@ def sign_plaintext(client_secret, resource_owner_secret): utilize the signature base string or the "oauth_timestamp" and "oauth_nonce" parameters. - .. _`section 3.4.4`: http://tools.ietf.org/html/rfc5849#section-3.4.4 + .. _`section 3.4.4`: https://tools.ietf.org/html/rfc5849#section-3.4.4 """ @@ -587,7 +587,7 @@ def sign_plaintext(client_secret, resource_owner_secret): # 1. The client shared-secret, after being encoded (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 signature = utils.escape(client_secret or '') # 2. An "&" character (ASCII code 38), which MUST be included even @@ -596,7 +596,7 @@ def sign_plaintext(client_secret, resource_owner_secret): # 3. The token shared-secret, after being encoded (`Section 3.6`_). # - # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 signature += utils.escape(resource_owner_secret or '') return signature @@ -612,7 +612,7 @@ def verify_hmac_sha1(request, client_secret=None, Per `section 3.4`_ of the spec. - .. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4 + .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4 To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri attribute MUST be an absolute URI whose netloc part identifies the @@ -620,7 +620,7 @@ def verify_hmac_sha1(request, client_secret=None, item of the request argument's headers dict attribute will be ignored. - .. _`RFC2616 section 5.2`: http://tools.ietf.org/html/rfc2616#section-5.2 + .. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2 """ norm_params = normalize_parameters(request.params) @@ -646,7 +646,7 @@ def verify_rsa_sha1(request, rsa_public_key): Note this method requires the jwt and cryptography libraries. - .. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3 + .. _`section 3.4.3`: https://tools.ietf.org/html/rfc5849#section-3.4.3 To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri attribute MUST be an absolute URI whose netloc part identifies the @@ -654,7 +654,7 @@ def verify_rsa_sha1(request, rsa_public_key): item of the request argument's headers dict attribute will be ignored. - .. _`RFC2616 section 5.2`: http://tools.ietf.org/html/rfc2616#section-5.2 + .. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2 """ norm_params = normalize_parameters(request.params) uri = normalize_base_string_uri(request.uri) @@ -675,7 +675,7 @@ def verify_plaintext(request, client_secret=None, resource_owner_secret=None): Per `section 3.4`_ of the spec. - .. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4 + .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4 """ signature = sign_plaintext(client_secret, resource_owner_secret) match = safe_string_equals(signature, request.signature) diff --git a/oauthlib/oauth1/rfc5849/utils.py b/oauthlib/oauth1/rfc5849/utils.py index 979e5f64..3762e3b5 100644 --- a/oauthlib/oauth1/rfc5849/utils.py +++ b/oauthlib/oauth1/rfc5849/utils.py @@ -49,7 +49,7 @@ def escape(u): Per `section 3.6`_ of the spec. - .. _`section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + .. _`section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6 """ if not isinstance(u, unicode_type): diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py index 7505b0d8..cbad8b74 100644 --- a/oauthlib/oauth2/rfc6749/clients/backend_application.py +++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py @@ -52,9 +52,9 @@ def prepare_request_body(self, body='', scope=None, **kwargs): >>> client.prepare_request_body(scope=['hello', 'world']) 'grant_type=client_credentials&scope=hello+world' - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ return prepare_token_request('client_credentials', body=body, scope=scope, **kwargs) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 5c5aceeb..a07a5c99 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -173,8 +173,8 @@ def add_token(self, uri, http_method='GET', body=None, headers=None, nonce="274312:dj83hs9s", mac="kDZvddkndxvhGRXZhvuDjEWhGeE=" - .. _`I-D.ietf-oauth-v2-bearer`: http://tools.ietf.org/html/rfc6749#section-12.2 - .. _`I-D.ietf-oauth-v2-http-mac`: http://tools.ietf.org/html/rfc6749#section-12.2 + .. _`I-D.ietf-oauth-v2-bearer`: https://tools.ietf.org/html/rfc6749#section-12.2 + .. _`I-D.ietf-oauth-v2-http-mac`: https://tools.ietf.org/html/rfc6749#section-12.2 """ if not is_secure_transport(uri): raise InsecureTransportError() @@ -401,9 +401,9 @@ def parse_request_body_response(self, body, scope=None, **kwargs): Providers may supply this in all responses but are required to only if it has changed since the authorization request. - .. _`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 + .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2 + .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1 """ 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 57fe99e8..b16fc9f8 100644 --- a/oauthlib/oauth2/rfc6749/clients/legacy_application.py +++ b/oauthlib/oauth2/rfc6749/clients/legacy_application.py @@ -64,9 +64,9 @@ def prepare_request_body(self, username, password, body='', scope=None, **kwargs >>> client.prepare_request_body(username='foo', password='bar', scope=['hello', 'world']) 'grant_type=password&username=foo&scope=hello+world&password=bar' - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ return prepare_token_request('password', body=body, username=username, password=password, scope=scope, **kwargs) diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py index 490efcd1..311aacf8 100644 --- a/oauthlib/oauth2/rfc6749/clients/mobile_application.py +++ b/oauthlib/oauth2/rfc6749/clients/mobile_application.py @@ -85,11 +85,11 @@ def prepare_request_uri(self, uri, redirect_uri=None, scope=None, >>> client.prepare_request_uri('https://example.com', foo='bar') 'https://example.com?client_id=your_id&response_type=token&foo=bar' - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 - .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12 """ return prepare_grant_uri(uri, self.client_id, 'token', redirect_uri=redirect_uri, state=state, scope=scope, **kwargs) @@ -164,8 +164,8 @@ def parse_request_uri_response(self, uri, state=None, scope=None): >>> 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 + .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 """ self.token = parse_implicit_response(uri, state=state, scope=scope) self._populate_attributes(self.token) diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index e6c32703..84ea0e99 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -136,7 +136,7 @@ def prepare_request_body(self, eyJpc3Mi[...omitted for brevity...]. J9l-ZhwP[...omitted for brevity...] - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ import jwt diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index bc62c8fb..14b5265f 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -76,11 +76,11 @@ def prepare_request_uri(self, uri, redirect_uri=None, scope=None, >>> client.prepare_request_uri('https://example.com', foo='bar') 'https://example.com?client_id=your_id&response_type=code&foo=bar' - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 - .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12 """ return prepare_grant_uri(uri, self.client_id, 'code', redirect_uri=redirect_uri, scope=scope, state=state, **kwargs) @@ -120,8 +120,8 @@ def prepare_request_body(self, client_id=None, code=None, body='', >>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar') 'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar' - .. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1 - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + .. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1 + .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ code = code or self.code return prepare_token_request('authorization_code', code=code, body=body, diff --git a/oauthlib/oauth2/rfc6749/endpoints/authorization.py b/oauthlib/oauth2/rfc6749/endpoints/authorization.py index b6e0734d..92cde346 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/authorization.py +++ b/oauthlib/oauth2/rfc6749/endpoints/authorization.py @@ -59,7 +59,7 @@ class AuthorizationEndpoint(BaseEndpoint): # Enforced through the design of oauthlib.common.Request - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B """ def __init__(self, default_response_type, default_token_type, diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py index 4364b81b..d5b5b782 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py +++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py @@ -5,7 +5,7 @@ An implementation of the OAuth 2 `Token Revocation`_ spec (draft 11). -.. _`Token Revocation`: http://tools.ietf.org/html/draft-ietf-oauth-revocation-11 +.. _`Token Revocation`: https://tools.ietf.org/html/draft-ietf-oauth-revocation-11 """ from __future__ import absolute_import, unicode_literals @@ -110,11 +110,11 @@ def validate_revocation_request(self, request): The client also includes its authentication credentials as described in `Section 2.3`_. of [`RFC6749`_]. - .. _`section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4 - .. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5 - .. _`section 2.3`: http://tools.ietf.org/html/rfc6749#section-2.3 - .. _`Section 4.1.2`: http://tools.ietf.org/html/draft-ietf-oauth-revocation-11#section-4.1.2 - .. _`RFC6749`: http://tools.ietf.org/html/rfc6749 + .. _`section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4 + .. _`section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5 + .. _`section 2.3`: https://tools.ietf.org/html/rfc6749#section-2.3 + .. _`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 """ if not request.token: raise InvalidRequestError(request=request, diff --git a/oauthlib/oauth2/rfc6749/endpoints/token.py b/oauthlib/oauth2/rfc6749/endpoints/token.py index ece63257..90fb16f4 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/token.py +++ b/oauthlib/oauth2/rfc6749/endpoints/token.py @@ -59,7 +59,7 @@ class TokenEndpoint(BaseEndpoint): # Delegated to each grant type. - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B """ def __init__(self, default_grant_type, default_token_type, grant_types): diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 8661c35b..7bea6507 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -91,7 +91,7 @@ class AuthorizationCodeGrant(GrantTypeBase): step (C). If valid, the authorization server responds back with an access token and, optionally, a refresh token. - .. _`Authorization Code Grant`: http://tools.ietf.org/html/rfc6749#section-4.1 + .. _`Authorization Code Grant`: https://tools.ietf.org/html/rfc6749#section-4.1 """ default_response_mode = 'query' @@ -175,11 +175,11 @@ def create_authorization_response(self, request, token_handler): File "oauthlib/oauth2/rfc6749/grant_types.py", line 591, in validate_authorization_request oauthlib.oauth2.rfc6749.errors.InvalidClientIdError - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 - .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12 """ try: # request.scopes is only mandated in post auth and both pre and @@ -206,7 +206,7 @@ def create_authorization_response(self, request, token_handler): # the authorization server informs the client by adding the following # parameters to the query component of the redirection URI using the # "application/x-www-form-urlencoded" format, per Appendix B: - # http://tools.ietf.org/html/rfc6749#appendix-B + # https://tools.ietf.org/html/rfc6749#appendix-B except errors.OAuth2Error as e: log.debug('Client error during validation of %r. %r.', request, e) request.redirect_uri = request.redirect_uri or self.error_uri @@ -285,7 +285,7 @@ def validate_authorization_request(self, request): raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request) # REQUIRED. The client identifier as described in Section 2.2. - # http://tools.ietf.org/html/rfc6749#section-2.2 + # https://tools.ietf.org/html/rfc6749#section-2.2 if not request.client_id: raise errors.MissingClientIdError(request=request) @@ -293,7 +293,7 @@ def validate_authorization_request(self, request): raise errors.InvalidClientIdError(request=request) # OPTIONAL. As described in Section 3.1.2. - # http://tools.ietf.org/html/rfc6749#section-3.1.2 + # https://tools.ietf.org/html/rfc6749#section-3.1.2 log.debug('Validating redirection uri %s for client %s.', request.redirect_uri, request.client_id) if request.redirect_uri is not None: @@ -320,7 +320,7 @@ def validate_authorization_request(self, request): # the authorization server informs the client by adding the following # parameters to the query component of the redirection URI using the # "application/x-www-form-urlencoded" format, per Appendix B. - # http://tools.ietf.org/html/rfc6749#appendix-B + # https://tools.ietf.org/html/rfc6749#appendix-B # Note that the correct parameters to be added are automatically # populated through the use of specific exceptions. @@ -346,7 +346,7 @@ def validate_authorization_request(self, request): raise errors.UnauthorizedClientError(request=request) # OPTIONAL. The scope of the access request as described by Section 3.3 - # http://tools.ietf.org/html/rfc6749#section-3.3 + # https://tools.ietf.org/html/rfc6749#section-3.3 self.validate_scopes(request) request_info.update({ @@ -384,14 +384,14 @@ def validate_token_request(self, request): # credentials (or assigned other authentication requirements), the # client MUST authenticate with the authorization server as described # in Section 3.2.1. - # http://tools.ietf.org/html/rfc6749#section-3.2.1 + # https://tools.ietf.org/html/rfc6749#section-3.2.1 if not self.request_validator.authenticate_client(request): log.debug('Client authentication failed, %r.', request) raise errors.InvalidClientError(request=request) elif not self.request_validator.authenticate_client_id(request.client_id, request): # REQUIRED, if the client is not authenticating with the # authorization server as described in Section 3.2.1. - # http://tools.ietf.org/html/rfc6749#section-3.2.1 + # https://tools.ietf.org/html/rfc6749#section-3.2.1 log.debug('Client authentication failed, %r.', request) raise errors.InvalidClientError(request=request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py index bf6c87f5..4c50a78a 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py @@ -47,7 +47,7 @@ class ClientCredentialsGrant(GrantTypeBase): (B) The authorization server authenticates the client, and if valid, issues an access token. - .. _`Client Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.4 + .. _`Client Credentials Grant`: https://tools.ietf.org/html/rfc6749#section-4.4 """ def create_token_response(self, request, token_handler): @@ -59,8 +59,8 @@ def create_token_response(self, request, token_handler): failed client authentication or is invalid, the authorization server returns an error response as described in `Section 5.2`_. - .. _`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 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2 """ headers = { 'Content-Type': 'application/json', diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py index 2b9c49d4..bdab8142 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py +++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py @@ -111,9 +111,9 @@ class ImplicitGrant(GrantTypeBase): See `Section 10.3`_ and `Section 10.16`_ for important security considerations when using the implicit grant. - .. _`Implicit Grant`: http://tools.ietf.org/html/rfc6749#section-4.2 - .. _`Section 10.3`: http://tools.ietf.org/html/rfc6749#section-10.3 - .. _`Section 10.16`: http://tools.ietf.org/html/rfc6749#section-10.16 + .. _`Implicit Grant`: https://tools.ietf.org/html/rfc6749#section-4.2 + .. _`Section 10.3`: https://tools.ietf.org/html/rfc6749#section-10.3 + .. _`Section 10.16`: https://tools.ietf.org/html/rfc6749#section-10.16 """ response_types = ['token'] @@ -152,11 +152,11 @@ def create_authorization_response(self, request, token_handler): access token matches a redirection URI registered by the client as described in `Section 3.1.2`_. - .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 - .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12 + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B """ return self.create_token_response(request, token_handler) @@ -195,9 +195,9 @@ def create_token_response(self, request, token_handler): The authorization server MUST NOT issue a refresh token. - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 + .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1 """ try: # request.scopes is only mandated in post auth and both pre and @@ -222,7 +222,7 @@ def create_token_response(self, request, token_handler): # the authorization server informs the client by adding the following # parameters to the fragment component of the redirection URI using the # "application/x-www-form-urlencoded" format, per Appendix B: - # http://tools.ietf.org/html/rfc6749#appendix-B + # https://tools.ietf.org/html/rfc6749#appendix-B except errors.OAuth2Error as e: log.debug('Client error during validation of %r. %r.', request, e) return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples, @@ -285,7 +285,7 @@ def validate_token_request(self, request): raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request) # REQUIRED. The client identifier as described in Section 2.2. - # http://tools.ietf.org/html/rfc6749#section-2.2 + # https://tools.ietf.org/html/rfc6749#section-2.2 if not request.client_id: raise errors.MissingClientIdError(request=request) @@ -293,7 +293,7 @@ def validate_token_request(self, request): raise errors.InvalidClientIdError(request=request) # OPTIONAL. As described in Section 3.1.2. - # http://tools.ietf.org/html/rfc6749#section-3.1.2 + # https://tools.ietf.org/html/rfc6749#section-3.1.2 if request.redirect_uri is not None: request.using_default_redirect_uri = False log.debug('Using provided redirect_uri %s', request.redirect_uri) @@ -304,7 +304,7 @@ def validate_token_request(self, request): # to which it will redirect the access token matches a # redirection URI registered by the client as described in # Section 3.1.2. - # http://tools.ietf.org/html/rfc6749#section-3.1.2 + # https://tools.ietf.org/html/rfc6749#section-3.1.2 if not self.request_validator.validate_redirect_uri( request.client_id, request.redirect_uri, request): raise errors.MismatchingRedirectURIError(request=request) @@ -328,7 +328,7 @@ def validate_token_request(self, request): # the authorization server informs the client by adding the following # parameters to the fragment component of the redirection URI using the # "application/x-www-form-urlencoded" format, per Appendix B. - # http://tools.ietf.org/html/rfc6749#appendix-B + # https://tools.ietf.org/html/rfc6749#appendix-B # Note that the correct parameters to be added are automatically # populated through the use of specific exceptions @@ -351,7 +351,7 @@ def validate_token_request(self, request): raise errors.UnauthorizedClientError(request=request) # OPTIONAL. The scope of the access request as described by Section 3.3 - # http://tools.ietf.org/html/rfc6749#section-3.3 + # https://tools.ietf.org/html/rfc6749#section-3.3 self.validate_scopes(request) request_info.update({ diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py index 6233e7cb..c2d86f79 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -19,7 +19,7 @@ class RefreshTokenGrant(GrantTypeBase): """`Refresh token grant`_ - .. _`Refresh token grant`: http://tools.ietf.org/html/rfc6749#section-6 + .. _`Refresh token grant`: https://tools.ietf.org/html/rfc6749#section-6 """ def __init__(self, request_validator=None, @@ -46,8 +46,8 @@ def create_token_response(self, request, token_handler): identical to that of the refresh token included by the client in the request. - .. _`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 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2 """ headers = { 'Content-Type': 'application/json', @@ -90,7 +90,7 @@ def validate_token_request(self, request): # the client was issued client credentials (or assigned other # authentication requirements), the client MUST authenticate with the # authorization server as described in Section 3.2.1. - # http://tools.ietf.org/html/rfc6749#section-3.2.1 + # https://tools.ietf.org/html/rfc6749#section-3.2.1 if self.request_validator.client_authentication_required(request): log.debug('Authenticating client, %r.', request) if not self.request_validator.authenticate_client(request): diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py index ede779a4..e5f04af4 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py @@ -67,7 +67,7 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase): the resource owner credentials, and if valid, issues an access token. - .. _`Resource Owner Password Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.3 + .. _`Resource Owner Password Credentials Grant`: https://tools.ietf.org/html/rfc6749#section-4.3 """ def create_token_response(self, request, token_handler): @@ -79,8 +79,8 @@ def create_token_response(self, request, token_handler): authentication or is invalid, the authorization server returns an error response as described in `Section 5.2`_. - .. _`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 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2 """ headers = { 'Content-Type': 'application/json', @@ -153,8 +153,8 @@ def validate_token_request(self, request): brute force attacks (e.g., using rate-limitation or generating alerts). - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ for validator in self.custom_validators.pre_token: validator(request) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index b87b1464..01079338 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -5,7 +5,7 @@ This module contains methods related to `Section 4`_ of the OAuth 2 RFC. -.. _`Section 4`: http://tools.ietf.org/html/rfc6749#section-4 +.. _`Section 4`: https://tools.ietf.org/html/rfc6749#section-4 """ from __future__ import absolute_import, unicode_literals @@ -61,11 +61,11 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None, &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 Host: server.example.com - .. _`W3C.REC-html401-19991224`: http://tools.ietf.org/html/rfc6749#ref-W3C.REC-html401-19991224 - .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 - .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 + .. _`W3C.REC-html401-19991224`: https://tools.ietf.org/html/rfc6749#ref-W3C.REC-html401-19991224 + .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12 """ if not is_secure_transport(uri): raise InsecureTransportError() @@ -111,7 +111,7 @@ def prepare_token_request(grant_type, body='', **kwargs): grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb - .. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1 + .. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1 """ params = [('grant_type', grant_type)] @@ -153,9 +153,9 @@ def prepare_token_revocation_request(url, token, token_type_hint="access_token", specification MAY define other values for this parameter using the registry defined in `Section 4.1.2`_. - .. _`Section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4 - .. _`Section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5 - .. _`Section 4.1.2`: http://tools.ietf.org/html/rfc7009#section-4.1.2 + .. _`Section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4 + .. _`Section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5 + .. _`Section 4.1.2`: https://tools.ietf.org/html/rfc7009#section-4.1.2 """ if not is_secure_transport(url): @@ -348,10 +348,10 @@ def parse_token_response(body, scope=None): "example_parameter":"example_value" } - .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 - .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`RFC4627`: http://tools.ietf.org/html/rfc4627 + .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1 + .. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6 + .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 + .. _`RFC4627`: https://tools.ietf.org/html/rfc4627 """ try: params = json.loads(body) @@ -359,7 +359,7 @@ def parse_token_response(body, scope=None): # Fall back to URL-encoded string, to support old implementations, # including (at time of writing) Facebook. See: - # https://github.com/idan/oauthlib/issues/267 + # https://github.com/oauthlib/oauthlib/issues/267 params = dict(urlparse.parse_qsl(body)) for key in ('expires_in', 'expires'): @@ -395,7 +395,7 @@ def validate_token_parameters(params): # If the issued access token scope is different from the one requested by # the client, the authorization server MUST include the "scope" response # parameter to inform the client of the actual scope granted. - # http://tools.ietf.org/html/rfc6749#section-3.3 + # https://tools.ietf.org/html/rfc6749#section-3.3 if params.scope_changed: message = 'Scope has changed from "{old}" to "{new}".'.format( old=params.old_scope, new=params.scope, diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index d25a6e05..182642e3 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -34,9 +34,9 @@ def client_authentication_required(self, request, *args, **kwargs): - Resource Owner Password Credentials Grant - Refresh Token Grant - .. _`Section 4.3.2`: http://tools.ietf.org/html/rfc6749#section-4.3.2 - .. _`Section 4.1.3`: http://tools.ietf.org/html/rfc6749#section-4.1.3 - .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6 + .. _`Section 4.3.2`: https://tools.ietf.org/html/rfc6749#section-4.3.2 + .. _`Section 4.1.3`: https://tools.ietf.org/html/rfc6749#section-4.1.3 + .. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6 """ return True @@ -60,7 +60,7 @@ def authenticate_client(self, request, *args, **kwargs): - Client Credentials Grant - Refresh Token Grant - .. _`HTTP Basic Authentication Scheme`: http://tools.ietf.org/html/rfc1945#section-11.1 + .. _`HTTP Basic Authentication Scheme`: https://tools.ietf.org/html/rfc1945#section-11.1 """ raise NotImplementedError('Subclasses must implement this method.') diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index e68ba597..4ae20e0e 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -4,8 +4,8 @@ This module contains methods for adding two types of access tokens to requests. -- Bearer http://tools.ietf.org/html/rfc6750 -- MAC http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01 +- Bearer https://tools.ietf.org/html/rfc6750 +- MAC https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01 """ from __future__ import absolute_import, unicode_literals @@ -93,8 +93,8 @@ def prepare_mac_header(token, uri, key, http_method, nonce="1336363200:dj83hs9s", mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM=" - .. _`MAC Access Authentication`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01 - .. _`extension algorithms`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1 + .. _`MAC Access Authentication`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01 + .. _`extension algorithms`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1 :param uri: Request URI. :param headers: Request headers as a dictionary. @@ -180,7 +180,7 @@ def prepare_bearer_uri(token, uri): http://www.example.com/path?access_token=h480djs93hd8 - .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750 + .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750 """ return add_params_to_uri(uri, [(('access_token', token))]) @@ -191,7 +191,7 @@ def prepare_bearer_headers(token, headers=None): Authorization: Bearer h480djs93hd8 - .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750 + .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750 """ headers = headers or {} headers['Authorization'] = 'Bearer %s' % token @@ -203,7 +203,7 @@ def prepare_bearer_body(token, body=''): access_token=h480djs93hd8 - .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750 + .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750 """ return add_params_to_qs(body, [(('access_token', token))]) diff --git a/setup.py b/setup.py index 4640ec80..0c4e5642 100755 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ def fread(fn): author_email='idan@gazit.me', maintainer='Ib Lundgren', maintainer_email='ib.lundgren@gmail.com', - url='https://github.com/idan/oauthlib', + url='https://github.com/oauthlib/oauthlib', platforms='any', license='BSD', packages=find_packages(exclude=('docs', 'tests', 'tests.*')), diff --git a/tox.ini b/tox.ini index a53676fb..2546beee 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36,pypy +envlist = py27,py34,py35,py36,pypy,docs [testenv] deps= @@ -9,3 +9,9 @@ commands=nosetests --with-coverage --cover-erase --cover-package=oauthlib -w tes [testenv:py27] deps=unittest2 {[testenv]deps} + +[testenv:docs] +deps=sphinx +changedir=docs +whitelist_externals=make +commands=make html From 3d248180e6fc66946821cce8938168cb15fc4f48 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Wed, 28 Feb 2018 14:00:39 +0000 Subject: [PATCH 018/237] Update repository location in Travis. (#514) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f5e9aca5..3290c1fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,4 +31,4 @@ deploy: distributions: sdist bdist_wheel on: tags: true - repo: idan/oauthlib + repo: oauthlib/oauthlib From d93403ad68ef308a195697fc79519df37812af1f Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Wed, 7 Mar 2018 16:08:52 +0000 Subject: [PATCH 019/237] Replace G+ with Gitter. (#517) --- README.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index b4892a80..dab88130 100644 --- a/README.rst +++ b/README.rst @@ -2,13 +2,14 @@ OAuthLib ======== *A generic, spec-compliant, thorough implementation of the OAuth request-signing -logic for python* +logic for Python 2.7 and 3.4+.* .. image:: https://travis-ci.org/oauthlib/oauthlib.svg?branch=master :target: https://travis-ci.org/oauthlib/oauthlib .. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master :target: https://coveralls.io/r/oauthlib/oauthlib - +.. image:: https://badges.gitter.im/oauthlib/oauthlib.svg + :target: https://gitter.im/oauthlib/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link OAuth often seems complicated and difficult-to-implement. There are several prominent libraries for handling OAuth requests, but they all suffer from one or @@ -33,10 +34,10 @@ Documentation Full documentation is available on `Read the Docs`_. All contributions are very welcome! The documentation is still quite sparse, please open an issue for what -you'd like to know, or discuss it in our `G+ community`_, or even better, send a +you'd like to know, or discuss it in our `Gitter community`_, or even better, send a pull request! -.. _`G+ community`: https://plus.google.com/communities/101889017375384052571 +.. _`Gitter community`: https://gitter.im/oauthlib/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link .. _`Read the Docs`: https://oauthlib.readthedocs.io/en/latest/index.html Interested in making OAuth requests? @@ -74,7 +75,7 @@ Patching OAuth support onto an http request framework? Creating an OAuth provider extension for a web framework? Simply using OAuthLib to Get Things Done or to learn? -No matter which we'd love to hear from you in our `G+ community`_ or if you have +No matter which we'd love to hear from you in our `Gitter community`_ or if you have anything in particular you would like to have, change or comment on don't hesitate for a second to send a pull request or open an issue. We might be quite busy and therefore slow to reply but we love feedback! @@ -83,7 +84,7 @@ Chances are you have run into something annoying that you wish there was documentation for, if you wish to gain eternal fame and glory, and a drink if we have the pleasure to run into eachother, please send a docs pull request =) -.. _`G+ community`: https://plus.google.com/communities/101889017375384052571 +.. _`Gitter community`: https://gitter.im/oauthlib/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link License ------- From 70b5827566dc3c0dd8f05cfbb8311b905f7a7254 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Mon, 12 Mar 2018 12:03:53 +0000 Subject: [PATCH 020/237] Add shields for Python versions, license and RTD. --- README.rst | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index dab88130..b477e418 100644 --- a/README.rst +++ b/README.rst @@ -6,10 +6,22 @@ logic for Python 2.7 and 3.4+.* .. image:: https://travis-ci.org/oauthlib/oauthlib.svg?branch=master :target: https://travis-ci.org/oauthlib/oauthlib + :alt: Travis .. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master :target: https://coveralls.io/r/oauthlib/oauthlib + :alt: Coveralls +.. image:: https://img.shields.io/pypi/pyversions/oauthlib.svg + :target: https://pypi.python.org/pypi/oauthlib + :alt: Download from PyPi +.. image:: https://img.shields.io/pypi/l/oauthlib.svg + :target: https://pypi.python.org/pypi/oauthlib + :alt: License +.. image:: https://img.shields.io/readthedocs/oauthlib.svg + :target: https://oauthlib.readthedocs.io/en/latest/index.html + :alt: Read the Docs .. image:: https://badges.gitter.im/oauthlib/oauthlib.svg - :target: https://gitter.im/oauthlib/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link + :target: https://gitter.im/oauthlib/Lobby + :alt: Chat on Gitter OAuth often seems complicated and difficult-to-implement. There are several prominent libraries for handling OAuth requests, but they all suffer from one or @@ -37,7 +49,7 @@ welcome! The documentation is still quite sparse, please open an issue for what you'd like to know, or discuss it in our `Gitter community`_, or even better, send a pull request! -.. _`Gitter community`: https://gitter.im/oauthlib/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link +.. _`Gitter community`: https://gitter.im/oauthlib/Lobby .. _`Read the Docs`: https://oauthlib.readthedocs.io/en/latest/index.html Interested in making OAuth requests? @@ -84,7 +96,7 @@ Chances are you have run into something annoying that you wish there was documentation for, if you wish to gain eternal fame and glory, and a drink if we have the pleasure to run into eachother, please send a docs pull request =) -.. _`Gitter community`: https://gitter.im/oauthlib/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link +.. _`Gitter community`: https://gitter.im/oauthlib/Lobby License ------- From f398fdb7b0ac7b0aa11408d11a4358524360ac77 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 18 Mar 2018 10:59:11 +0100 Subject: [PATCH 021/237] Fix ReadTheDocs build (#521) --- docs/conf.py | 3 +-- tox.ini | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b1ca34d9..017f6861 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,8 +14,6 @@ import os import sys -from oauthlib import __version__ as v - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -51,6 +49,7 @@ # # The short X.Y version. +from oauthlib import __version__ as v version = v[:3] # The full version, including alpha/beta/rc tags. release = v diff --git a/tox.ini b/tox.ini index 2546beee..3dded414 100644 --- a/tox.ini +++ b/tox.ini @@ -10,8 +10,12 @@ commands=nosetests --with-coverage --cover-erase --cover-package=oauthlib -w tes deps=unittest2 {[testenv]deps} +# tox -e docs to mimick readthedocs build. +# as of today, RTD is using python2.7 and doesn't run "setup.py install" [testenv:docs] +basepython=python2.7 +skipsdist=True deps=sphinx changedir=docs whitelist_externals=make -commands=make html +commands=make clean html From ad61175827cddda8f8cb3cccc14f9f5eb9887ca7 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sun, 18 Mar 2018 11:06:51 +0100 Subject: [PATCH 022/237] Fixed "make" command to test upstream with local oauthlib. (#522) --- Makefile | 74 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/Makefile b/Makefile index 259fe9ca..8571a911 100644 --- a/Makefile +++ b/Makefile @@ -1,47 +1,57 @@ -PYS = py27,py34,pypy +# Downstream tests (Don't be evil) +# +# Try and not break the libraries below by running their tests too. +# +# Unfortunately there is no neat way to run downstream tests AFAIK +# Until we have a proper downstream testing system we will +# stick to this Makefile. +#--------------------------- +# HOW TO ADD NEW DOWNSTREAM LIBRARIES +# +# Please specify your library as well as primary contacts. +# Since these contacts will be addressed with Github mentions they +# need to be Github users (for now)(sorry Bitbucket). +# +clean: + rm -rf .tox + rm -rf bottle-oauthlib + rm -rf django-oauth-toolkit + rm -rf flask-oauthlib + rm -rf requests-oauthlib test: - # Test OAuthLib - tox -e "$(PYS)" - # - # Downstream tests (Don't be evil) - # - # Try and not break the libraries below by running their tests too. - # - # Unfortunately there is no neat way to run downstream tests AFAIK - # Until we have a proper downstream testing system we will - # stick to this Makefile. + tox + +bottle: #--------------------------- - # HOW TO ADD NEW DOWNSTREAM LIBRARIES - # - # Please specify your library as well as primary contacts. - # Since these contacts will be addressed with Github mentions they - # need to be Github users (for now)(sorry Bitbucket). - # + # Library thomsonreuters/bottle-oauthlib + # Contacts: Jonathan.Huot + cd bottle-oauthlib 2>/dev/null || git clone https://github.com/thomsonreuters/bottle-oauthlib.git + cd bottle-oauthlib && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox + +flask: #--------------------------- # Library: lepture/flask-oauthLib # Contacts: lepture,widnyana - git clone https://github.com/lepture/flask-oauthlib.git - cd flask-oauthlib && cp ../tox.ini . && sed -i 's/py32,py33,py34,//' tox.ini && sed -i '/mock/a \ Flask-SQLAlchemy' tox.ini && tox -e "$(PYS)" - rm -rf flask-oauthlib + cd flask-oauthlib 2>/dev/null || git clone https://github.com/lepture/flask-oauthlib.git + cd flask-oauthlib && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox + +django: #--------------------------- # Library: evonove/django-oauth-toolkit # Contacts: evonove,masci # (note: has tox.ini already) - git clone https://github.com/evonove/django-oauth-toolkit.git - cd django-oauth-toolkit && tox -e "$(PYS)" - rm -rf django-oauth-toolkit + cd django-oauth-toolkit 2>/dev/null || git clone https://github.com/evonove/django-oauth-toolkit.git + cd django-oauth-toolkit && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && tox -e py27,py35,py36 + +requests: #--------------------------- # Library requests/requests-oauthlib # Contacts: ib-lundgren,lukasa - git clone https://github.com/requests/requests-oauthlib.git - cd requests-oauthlib && cp ../tox.ini . && sed -i '/mock/a \ requests' tox.ini && tox -e "$(PYS)" - rm -rf requests-oauthlib - #--------------------------- - # + cd requests-oauthlib 2>/dev/null || git clone https://github.com/requests/requests-oauthlib.git + cd requests-oauthlib && sed -i.old 's,deps=,deps = --editable=file://{toxinidir}/../[signedtoken],' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox -pycco: - find oauthlib -name "*.py" -exec pycco -p -s reST {} \; -pycco-clean: - rm -rf docs/oauthlib docs/pycco.css +.DEFAULT_GOAL := all +.PHONY: clean test bottle django flask requests +all: clean test bottle django flask requests From 9855c6b2a030a5307691837ba705000ec8f898f0 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 13 Mar 2018 09:37:29 +0100 Subject: [PATCH 023/237] Replace IRC notificatgion with Gitter Hook --- .travis.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3290c1fa..e3c01f15 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,12 @@ script: tox after_success: coveralls notifications: - irc: irc.freenode.org#oauthlib + webhooks: + urls: + - https://webhooks.gitter.im/e/6008c872bf0ecee344f4 + on_success: change + on_failure: always + on_start: never deploy: provider: pypi user: ib.lundgren From 3e13cd30b29c273a79824ad02f1d2cb8700a5955 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 13 Mar 2018 09:54:41 +0100 Subject: [PATCH 024/237] Added Github Releases deploy provider. --- .travis.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index e3c01f15..06506e72 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,11 +29,17 @@ notifications: on_failure: always on_start: never deploy: - provider: pypi - user: ib.lundgren - password: - secure: PGZF9pRiTGCSwQjk1ddTKF3x4rQ0iAiPbg2uSixyO68uMXRgJjwHhSrNM0OEqtK5YWU5FE5L0DwR1nkrpEJKO4a5q2EOgos+gVoKpJfinoUNOOkjc1VHpqKM0uRf/OKrw1alvWUwqvW8B+DOb9TY5c5VZxQuRL+iwdrtwzFlKls= - distributions: sdist bdist_wheel - on: - tags: true - repo: oauthlib/oauthlib + - provider: pypi + user: ib.lundgren + password: + secure: PGZF9pRiTGCSwQjk1ddTKF3x4rQ0iAiPbg2uSixyO68uMXRgJjwHhSrNM0OEqtK5YWU5FE5L0DwR1nkrpEJKO4a5q2EOgos+gVoKpJfinoUNOOkjc1VHpqKM0uRf/OKrw1alvWUwqvW8B+DOb9TY5c5VZxQuRL+iwdrtwzFlKls= + distributions: sdist bdist_wheel + on: + tags: true + repo: oauthlib/oauthlib + - provider: releases + api_key: "$GITHUB_OAUTH_TOKEN" + skip_cleanup: true + on: + tags: true + repo: oauthlib/oauthlib From 649029506c354818c946a7d139a9a0a8054317ca Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Fri, 9 Mar 2018 20:16:42 +0000 Subject: [PATCH 025/237] Update requirements. --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index e4980c7a..a4614bbd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -pyjwt==1.0.0 -blinker==1.3 -cryptography>=0.8.1 +pyjwt==1.6.0 +blinker==1.4 +cryptography>=1.4.0 From ec0e618587b0cf1cda26252fe3aae48dc69a17da Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 19 Mar 2018 13:14:25 +0100 Subject: [PATCH 026/237] Fixed pypi credentials since oauthlib move. (#527) --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 06506e72..244862b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,9 +30,9 @@ notifications: on_start: never deploy: - provider: pypi - user: ib.lundgren + user: JonathanHuot password: - secure: PGZF9pRiTGCSwQjk1ddTKF3x4rQ0iAiPbg2uSixyO68uMXRgJjwHhSrNM0OEqtK5YWU5FE5L0DwR1nkrpEJKO4a5q2EOgos+gVoKpJfinoUNOOkjc1VHpqKM0uRf/OKrw1alvWUwqvW8B+DOb9TY5c5VZxQuRL+iwdrtwzFlKls= + secure: "OozNM16flVLvqDoNzmoTENchhS1w0/dEJZvXBQK2KWmh8fyGj2UZus1vkl6bA5V3Yu9MZLYFpDcltl/qraY3Up6iXQpwKz4q+ICygAudYM2kJ5l8ZEe+wy2FikWbD6LkXf5uKIJJnPNSC8AI86ZyxM/XZxbYjj/+jXyJ1YFZwwQ=" distributions: sdist bdist_wheel on: tags: true From e2f40a9ec3159f9ce326623004480f64c2167b76 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Mon, 19 Mar 2018 13:36:55 +0000 Subject: [PATCH 027/237] Fix Travis config (#529) * Fix indentation in Travis config. * Fill GitHub OAuth key. * Deploy tags from all branches. --- .travis.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 244862b3..043f4356 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,13 +33,16 @@ deploy: user: JonathanHuot password: secure: "OozNM16flVLvqDoNzmoTENchhS1w0/dEJZvXBQK2KWmh8fyGj2UZus1vkl6bA5V3Yu9MZLYFpDcltl/qraY3Up6iXQpwKz4q+ICygAudYM2kJ5l8ZEe+wy2FikWbD6LkXf5uKIJJnPNSC8AI86ZyxM/XZxbYjj/+jXyJ1YFZwwQ=" - distributions: sdist bdist_wheel - on: - tags: true - repo: oauthlib/oauthlib + distributions: sdist bdist_wheel + on: + tags: true + all_branches: true + repo: oauthlib/oauthlib - provider: releases - api_key: "$GITHUB_OAUTH_TOKEN" + api_key: + secure: "YwPR/xHcqKdBAo8bSfJqAj2aD8zEdVYbbeAoo+cl1OZkRnCC1WnsfXw865wC/gi/sSQUFvdHFGGdy5CY1Iv3E67W2CiJDN0X6AxQPrq50WUzk/pyUyvIBhsHKMU+FDYmRGU2cwOy8jlun9V9fzC2LkA9yyAklisePbIg0I3mhKA=" skip_cleanup: true on: tags: true + all_branches: true repo: oauthlib/oauthlib From 2a1992420970fbd2299a3aae834873c63e43fd6c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 19 Mar 2018 14:49:43 +0100 Subject: [PATCH 028/237] Fixed indentation after travis setup --- .travis.yml | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 043f4356..041510d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,30 +1,27 @@ language: python sudo: false cache: pip - matrix: include: - - python: 2.7 - env: TOXENV=py27 - - python: 3.4 - env: TOXENV=py34 - - python: 3.5 - env: TOXENV=py35 - - python: 3.6 - env: TOXENV=py36 - - python: pypy-5.3 - env: TOXENV=pypy - + - python: 2.7 + env: TOXENV=py27 + - python: 3.4 + env: TOXENV=py34 + - python: 3.5 + env: TOXENV=py35 + - python: 3.6 + env: TOXENV=py36 + - python: pypy-5.3 + env: TOXENV=pypy install: - - pip install -U setuptools - - pip install tox coveralls +- pip install -U setuptools +- pip install tox coveralls script: tox - after_success: coveralls notifications: webhooks: urls: - - https://webhooks.gitter.im/e/6008c872bf0ecee344f4 + - https://webhooks.gitter.im/e/6008c872bf0ecee344f4 on_success: change on_failure: always on_start: never From 2128054a130ebdedfdee9b80801bfa4ad257c1ee Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 19 Mar 2018 14:51:09 +0100 Subject: [PATCH 029/237] Generated api_key from travis setup releases --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 041510d6..0a7d8ad3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ deploy: repo: oauthlib/oauthlib - provider: releases api_key: - secure: "YwPR/xHcqKdBAo8bSfJqAj2aD8zEdVYbbeAoo+cl1OZkRnCC1WnsfXw865wC/gi/sSQUFvdHFGGdy5CY1Iv3E67W2CiJDN0X6AxQPrq50WUzk/pyUyvIBhsHKMU+FDYmRGU2cwOy8jlun9V9fzC2LkA9yyAklisePbIg0I3mhKA=" + secure: LEzTaeQt4+Sp21t7usmwaEYLThKIGWDNNj04JADMLgfquTeyz5nDu9P8JNlT//G9RNN20oR8w7jZo97Y+JAylq6Hh/I+p/MEzZi8+NwIpObk3n3zJO4witZQQSTEw/6B7qf1/NQQxjQzlYTJjsGXxBps7srviWZmbH6Tz+epA3A= skip_cleanup: true on: tags: true From 43b66d8e60ed44b40b895f7e6d974665ccb43f82 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Fri, 9 Mar 2018 21:03:15 +0000 Subject: [PATCH 030/237] Version bump 2.0.7. (cherry picked from commit 67ebd7a) --- CHANGELOG.rst | 17 +++++++++++++++++ oauthlib/__init__.py | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8a20f92d..9ced51ac 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,23 @@ Changelog ========= +2.0.7 (2018-03-09) +------------------ + +* Moved oauthlib into new organization on GitHub. +* Include license file in the generated wheel package. (#494) +* When deploying a release to PyPI, include the wheel distribution. (#496) +* Check access token in self.token dict. (#500) +* Added bottle-oauthlib to docs. (#509) +* Updated docs for organization change. (#515) +* Update repository location in Travis. (#514) +* Replace G+ with Gitter. (#517) + +2.0.6 (2017-10-20) +------------------ + +* 2.0.5 contains breaking changes. + 2.0.5 (2017-10-19) ------------------ diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 459f307e..36450108 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -9,8 +9,8 @@ :license: BSD, see LICENSE for details. """ -__author__ = 'Idan Gazit ' -__version__ = '2.0.5' +__author__ = 'The OAuthlib Community' +__version__ = '2.0.7' import logging From 9d398229755e05e07c4d063a167a858236a200c5 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Sun, 18 Mar 2018 10:35:08 +0000 Subject: [PATCH 031/237] Update changelog for 2.0.7. (cherry picked from commit e7b906a) --- AUTHORS | 2 ++ CHANGELOG.rst | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 811679e0..7d5d9ada 100644 --- a/AUTHORS +++ b/AUTHORS @@ -26,3 +26,5 @@ Juan Fabio García Solero Omer Katz Joel Stevenson Brendan McCollam +Jonathan Huot +Pieter Ennes diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9ced51ac..7389af0f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -2.0.7 (2018-03-09) +2.0.7 (2018-03-19) ------------------ * Moved oauthlib into new organization on GitHub. @@ -9,9 +9,15 @@ Changelog * When deploying a release to PyPI, include the wheel distribution. (#496) * Check access token in self.token dict. (#500) * Added bottle-oauthlib to docs. (#509) -* Updated docs for organization change. (#515) * Update repository location in Travis. (#514) +* Updated docs for organization change. (#515) * Replace G+ with Gitter. (#517) +* Update requirements. (#518) +* Add shields for Python versions, license and RTD. (#520) +* Fix ReadTheDocs build (#521). +* Fixed "make" command to test upstream with local oauthlib. (#522) +* Replace IRC notification with Gitter Hook. (#523) +* Added Github Releases deploy provider. (#523) 2.0.6 (2017-10-20) ------------------ From d49b9f02a821dca920c89b24540485da3b96bf1e Mon Sep 17 00:00:00 2001 From: Jimmy Thrasibule Date: Fri, 13 Apr 2018 04:27:01 -0400 Subject: [PATCH 032/237] Add request argument to confirm_redirect_uri (#504) (#504) --- examples/skeleton_oauth2_web_application_server.py | 2 +- oauthlib/oauth2/rfc6749/grant_types/authorization_code.py | 3 ++- oauthlib/oauth2/rfc6749/request_validator.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/skeleton_oauth2_web_application_server.py b/examples/skeleton_oauth2_web_application_server.py index 8bfd9369..e53232f3 100644 --- a/examples/skeleton_oauth2_web_application_server.py +++ b/examples/skeleton_oauth2_web_application_server.py @@ -67,7 +67,7 @@ def validate_code(self, client_id, code, client, request, *args, **kwargs): # state and user to request.scopes and request.user. pass - def confirm_redirect_uri(self, client_id, code, redirect_uri, client, *args, **kwargs): + def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request, *args, **kwargs): # You did save the redirect uri with the authorization code right? pass diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 7bea6507..06602634 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -421,7 +421,8 @@ def validate_token_request(self, request): # authorization request as described in Section 4.1.1, and their # values MUST be identical. if not self.request_validator.confirm_redirect_uri(request.client_id, request.code, - request.redirect_uri, request.client): + request.redirect_uri, request.client, + request): log.debug('Redirect_uri (%r) invalid for client %r (%r).', request.redirect_uri, request.client_id, request.client) raise errors.MismatchingRedirectURIError(request=request) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 182642e3..c0b69a1a 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -82,7 +82,7 @@ def authenticate_client_id(self, client_id, request, *args, **kwargs): """ raise NotImplementedError('Subclasses must implement this method.') - def confirm_redirect_uri(self, client_id, code, redirect_uri, client, + def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request, *args, **kwargs): """Ensure that the authorization process represented by this authorization code began with this 'redirect_uri'. From d21fd53e13c044ad034694ee93e97eb7c4aac101 Mon Sep 17 00:00:00 2001 From: Olaf Conradi Date: Fri, 13 Apr 2018 10:32:01 +0200 Subject: [PATCH 033/237] Use secrets module in Python 3.6 and later (#533) The secrets module should be used for generating cryptographically strong random numbers suitable for managing data such as passwords, account authentication, security tokens, and related secrets. In particularly, secrets should be used in preference to the default pseudo-random number generator in the random module, which is designed for modelling and simulation, not security or cryptography. --- AUTHORS | 1 + docs/oauth1/security.rst | 12 +++++++----- oauthlib/common.py | 11 ++++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/AUTHORS b/AUTHORS index 7d5d9ada..f52ce9ad 100644 --- a/AUTHORS +++ b/AUTHORS @@ -28,3 +28,4 @@ Joel Stevenson Brendan McCollam Jonathan Huot Pieter Ennes +Olaf Conradi diff --git a/docs/oauth1/security.rst b/docs/oauth1/security.rst index a1432a92..df1e2a0e 100644 --- a/docs/oauth1/security.rst +++ b/docs/oauth1/security.rst @@ -16,11 +16,13 @@ A few important facts regarding OAuth security * **Tokens must be random**, OAuthLib provides a method for generating secure tokens and it's packed into ``oauthlib.common.generate_token``, - use it. If you decide to roll your own, use ``random.SystemRandom`` - which is based on ``os.urandom`` rather than the default ``random`` - based on the effecient but not truly random Mersenne Twister. - Predictable tokens allow attackers to bypass virtually all defences - OAuth provides. + use it. If you decide to roll your own, use ``secrets.SystemRandom`` + for Python 3.6 and later. The ``secrets`` module is designed for + generating cryptographically strong random numbers. For earlier versions + of Python, use ``random.SystemRandom`` which is based on ``os.urandom`` + rather than the default ``random`` based on the effecient but not truly + random Mersenne Twister. Predictable tokens allow attackers to bypass + virtually all defences OAuth provides. * **Timing attacks are real** and more than possible if you host your application inside a shared datacenter. Ensure all ``validate_`` methods diff --git a/oauthlib/common.py b/oauthlib/common.py index afcc09ca..f25656ff 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -11,11 +11,16 @@ import collections import datetime import logging -import random import re import sys import time +try: + from secrets import randbits + from secrets import SystemRandom +except ImportError: + from random import getrandbits as randbits + from random import SystemRandom try: from urllib import quote as _quote from urllib import unquote as _unquote @@ -202,7 +207,7 @@ def generate_nonce(): .. _`section 3.2.1`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1 .. _`section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3 """ - return unicode_type(unicode_type(random.getrandbits(64)) + generate_timestamp()) + return unicode_type(unicode_type(randbits(64)) + generate_timestamp()) def generate_timestamp(): @@ -225,7 +230,7 @@ def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET): and entropy when generating the random characters is important. Which is why SystemRandom is used instead of the default random.choice method. """ - rand = random.SystemRandom() + rand = SystemRandom() return ''.join(rand.choice(chars) for x in range(length)) From 1b3498aeac6f4c57156283e59d340746595d6329 Mon Sep 17 00:00:00 2001 From: paulie4 Date: Fri, 13 Apr 2018 04:39:07 -0400 Subject: [PATCH 034/237] Fixed some copy and paste typos (#535) Fixed some copy and paste typos, see issue #532. --- oauthlib/oauth2/rfc6749/clients/service_application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index 84ea0e99..7f336bbc 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -146,8 +146,8 @@ def prepare_request_body(self, ' token requests.') claim = { 'iss': issuer or self.issuer, - 'aud': audience or self.issuer, - 'sub': subject or self.issuer, + 'aud': audience or self.audience, + 'sub': subject or self.subject, 'exp': int(expires_at or time.time() + 3600), 'iat': int(issued_at or time.time()), } From 657065d76d59a100ffcacd0954fb2091552dfaa2 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Tue, 8 May 2018 21:14:35 +0100 Subject: [PATCH 035/237] Avoid populating spurious token credentials (#542) --- oauthlib/oauth2/rfc6749/clients/base.py | 19 ++++++++++++------- .../rfc6749/clients/mobile_application.py | 2 +- .../oauth2/rfc6749/clients/web_application.py | 2 +- .../clients/test_mobile_application.py | 12 ++++++++++++ .../rfc6749/clients/test_web_application.py | 19 +++++++++++++++++++ 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index a07a5c99..3c5372c4 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -111,8 +111,10 @@ def __init__(self, client_id, self.state_generator = state_generator self.state = state self.redirect_url = redirect_url + self.code = None + self.expires_in = None self._expires_at = None - self._populate_attributes(self.token) + self._populate_token_attributes(self.token) @property def token_types(self): @@ -406,7 +408,7 @@ def parse_request_body_response(self, body, scope=None, **kwargs): .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1 """ self.token = parse_token_response(body, scope=scope) - self._populate_attributes(self.token) + self._populate_token_attributes(self.token) return self.token def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs): @@ -459,8 +461,14 @@ def _add_mac_token(self, uri, http_method='GET', body=None, hash_algorithm=self.mac_algorithm, **kwargs) return uri, headers, body - def _populate_attributes(self, response): - """Add commonly used values such as access_token to self.""" + def _populate_code_attributes(self, response): + """Add attributes from an auth code response to self.""" + + if 'code' in response: + self.code = response.get('code') + + def _populate_token_attributes(self, response): + """Add attributes from a token exchange response to self.""" if 'access_token' in response: self.access_token = response.get('access_token') @@ -478,9 +486,6 @@ def _populate_attributes(self, response): if 'expires_at' in response: self._expires_at = int(response.get('expires_at')) - if 'code' in response: - self.code = response.get('code') - if 'mac_key' in response: self.mac_key = response.get('mac_key') diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py index 311aacf8..965185d1 100644 --- a/oauthlib/oauth2/rfc6749/clients/mobile_application.py +++ b/oauthlib/oauth2/rfc6749/clients/mobile_application.py @@ -168,5 +168,5 @@ def parse_request_uri_response(self, uri, state=None, scope=None): .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 """ self.token = parse_implicit_response(uri, state=state, scope=scope) - self._populate_attributes(self.token) + self._populate_token_attributes(self.token) return self.token diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index 14b5265f..435c0b1f 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -172,5 +172,5 @@ def parse_request_uri_response(self, uri, state=None): oauthlib.oauth2.rfc6749.errors.MismatchingStateError """ response = parse_authorization_code_response(uri, state=state) - self._populate_attributes(response) + self._populate_code_attributes(response) return response diff --git a/tests/oauth2/rfc6749/clients/test_mobile_application.py b/tests/oauth2/rfc6749/clients/test_mobile_application.py index 309220ba..51e4dab3 100644 --- a/tests/oauth2/rfc6749/clients/test_mobile_application.py +++ b/tests/oauth2/rfc6749/clients/test_mobile_application.py @@ -69,6 +69,18 @@ def test_implicit_token_uri(self): uri = client.prepare_request_uri(self.uri, **self.kwargs) self.assertURLEqual(uri, self.uri_kwargs) + def test_populate_attributes(self): + + client = MobileApplicationClient(self.client_id) + + response_uri = (self.response_uri + "&code=EVIL-CODE") + + client.parse_request_uri_response(response_uri, scope=self.scope) + + # We must not accidentally pick up any further security + # credentials at this point. + self.assertIsNone(client.code) + def test_parse_token_response(self): client = MobileApplicationClient(self.client_id) diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 0a80c9ad..4ecc3b30 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -117,6 +117,25 @@ def test_parse_grant_uri_response(self): self.response_uri, state="invalid") + def test_populate_attributes(self): + + client = WebApplicationClient(self.client_id) + + response_uri = (self.response_uri + + "&access_token=EVIL-TOKEN" + "&refresh_token=EVIL-TOKEN" + "&mac_key=EVIL-KEY") + + client.parse_request_uri_response(response_uri, self.state) + + self.assertEqual(client.code, self.code) + + # We must not accidentally pick up any further security + # credentials at this point. + self.assertIsNone(client.access_token) + self.assertIsNone(client.refresh_token) + self.assertIsNone(client.mac_key) + def test_parse_token_response(self): client = WebApplicationClient(self.client_id) From a9d9ba17a0fe04cec5afa1c6ede96f1984ae7334 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Fri, 18 May 2018 19:04:06 +0100 Subject: [PATCH 036/237] Backward compatibility fix for requests-oauthlib. (#546) --- oauthlib/oauth2/rfc6749/clients/base.py | 14 ++++++++++---- .../oauth2/rfc6749/clients/mobile_application.py | 2 +- oauthlib/oauth2/rfc6749/clients/web_application.py | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 3c5372c4..07ef8949 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -9,6 +9,7 @@ from __future__ import absolute_import, unicode_literals import time +import warnings from oauthlib.common import generate_token from oauthlib.oauth2.rfc6749 import tokens @@ -114,7 +115,7 @@ def __init__(self, client_id, self.code = None self.expires_in = None self._expires_at = None - self._populate_token_attributes(self.token) + self.populate_token_attributes(self.token) @property def token_types(self): @@ -408,7 +409,7 @@ def parse_request_body_response(self, body, scope=None, **kwargs): .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1 """ self.token = parse_token_response(body, scope=scope) - self._populate_token_attributes(self.token) + self.populate_token_attributes(self.token) return self.token def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs): @@ -461,13 +462,18 @@ def _add_mac_token(self, uri, http_method='GET', body=None, hash_algorithm=self.mac_algorithm, **kwargs) return uri, headers, body - def _populate_code_attributes(self, response): + def _populate_attributes(self, response): + warnings.warn("Please switch to the public method " + "populate_token_attributes.", DeprecationWarning) + return self.populate_token_attributes(response) + + def populate_code_attributes(self, response): """Add attributes from an auth code response to self.""" if 'code' in response: self.code = response.get('code') - def _populate_token_attributes(self, response): + def populate_token_attributes(self, response): """Add attributes from a token exchange response to self.""" if 'access_token' in response: diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py index 965185d1..aa20daab 100644 --- a/oauthlib/oauth2/rfc6749/clients/mobile_application.py +++ b/oauthlib/oauth2/rfc6749/clients/mobile_application.py @@ -168,5 +168,5 @@ def parse_request_uri_response(self, uri, state=None, scope=None): .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 """ self.token = parse_implicit_response(uri, state=state, scope=scope) - self._populate_token_attributes(self.token) + self.populate_token_attributes(self.token) return self.token diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index 435c0b1f..c14a5f8a 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -172,5 +172,5 @@ def parse_request_uri_response(self, uri, state=None): oauthlib.oauth2.rfc6749.errors.MismatchingStateError """ response = parse_authorization_code_response(uri, state=state) - self._populate_code_attributes(response) + self.populate_code_attributes(response) return response From 360e0c2ca2c97aacf615228534e9b8963f8359c2 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sun, 20 May 2018 07:44:08 +0300 Subject: [PATCH 037/237] Don't cover the fallback branch. --- oauthlib/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/signals.py b/oauthlib/signals.py index 2f866505..22d47a41 100644 --- a/oauthlib/signals.py +++ b/oauthlib/signals.py @@ -8,7 +8,7 @@ try: from blinker import Namespace signals_available = True -except ImportError: +except ImportError: # noqa class Namespace(object): def signal(self, name, doc=None): return _FakeSignal(name, doc) From 6fc8ba83dcf9b644ab26d8b41cd0f8d74624dabd Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sun, 20 May 2018 07:53:18 +0300 Subject: [PATCH 038/237] Added .coveragerc. --- .coveragerc | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..c2c282e2 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,21 @@ +[run] +branch = 1 +cover_pylib = 0 +include=*oauthlib/* +omit = oauthlib.tests.* + +[report] +omit = + */python?.?/* + */site-packages/* + */pypy/* +[report] +exclude_lines = + pragma: no cover + def __repr__ + if __debug__: + raise AssertionError + raise NotImplementedError + if 0: + if __name__ == .__main__.: + noqa From abb9f8fdf96ab0467eb9ca6d1fb15bfbe87c4207 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sun, 20 May 2018 07:55:10 +0300 Subject: [PATCH 039/237] Fix .coveragerc. --- .coveragerc | 1 - 1 file changed, 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index c2c282e2..70666c7a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -9,7 +9,6 @@ omit = */python?.?/* */site-packages/* */pypy/* -[report] exclude_lines = pragma: no cover def __repr__ From d7a9cf556e6794c0debb1af1d444e98375d0577e Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sun, 20 May 2018 08:00:11 +0300 Subject: [PATCH 040/237] Ignore Python 2.7 fallback branch. --- oauthlib/oauth1/rfc5849/parameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/oauth1/rfc5849/parameters.py b/oauthlib/oauth1/rfc5849/parameters.py index 2f068a7c..db4400e9 100644 --- a/oauthlib/oauth1/rfc5849/parameters.py +++ b/oauthlib/oauth1/rfc5849/parameters.py @@ -15,7 +15,7 @@ try: from urlparse import urlparse, urlunparse -except ImportError: +except ImportError: # noqa from urllib.parse import urlparse, urlunparse From a306b12b98b5d2aaf469b89b956db0df050823e7 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Mon, 21 May 2018 07:15:22 +0200 Subject: [PATCH 041/237] Add test coverage (#544) * Add testcase for prepare_token_request() * Add testcase for InsecureTransportError in add_token() * Fix typo in testcase of add_token() for MAC token type * Add testcase for TokenExpiredError in add_token() * Add testcase for prepare_request_body without private key * Add testcase for optional kwargs in prepare_request_body() --- tests/oauth2/rfc6749/clients/test_base.py | 72 ++++++++++++++++++- .../clients/test_service_application.py | 70 +++++++++++++++++- 2 files changed, 137 insertions(+), 5 deletions(-) diff --git a/tests/oauth2/rfc6749/clients/test_base.py b/tests/oauth2/rfc6749/clients/test_base.py index c788bc1b..d48a944f 100644 --- a/tests/oauth2/rfc6749/clients/test_base.py +++ b/tests/oauth2/rfc6749/clients/test_base.py @@ -4,7 +4,7 @@ import datetime from oauthlib import common -from oauthlib.oauth2 import Client, InsecureTransportError +from oauthlib.oauth2 import Client, InsecureTransportError, TokenExpiredError from oauthlib.oauth2.rfc6749 import utils from oauthlib.oauth2.rfc6749.clients import AUTH_HEADER, BODY, URI_QUERY @@ -51,10 +51,26 @@ def test_add_bearer_token(self): self.assertFormBodyEqual(body, self.body) self.assertEqual(headers, self.bearer_header) + # Non-HTTPS + insecure_uri = 'http://example.com/path?query=world' + client = Client(self.client_id, access_token=self.access_token, token_type="Bearer") + self.assertRaises(InsecureTransportError, client.add_token, insecure_uri, + body=self.body, + headers=self.headers) + # Missing access token client = Client(self.client_id) self.assertRaises(ValueError, client.add_token, self.uri) + # Expired token + expired = 523549800 + expired_token = { + 'expires_at': expired, + } + client = Client(self.client_id, token=expired_token, access_token=self.access_token, token_type="Bearer") + self.assertRaises(TokenExpiredError, client.add_token, self.uri, + body=self.body, headers=self.headers) + # The default token placement, bearer in auth header client = Client(self.client_id, access_token=self.access_token) uri, headers, body = client.add_token(self.uri, body=self.body, @@ -150,8 +166,26 @@ def test_add_mac_token(self): self.assertEqual(uri, self.uri) self.assertEqual(body, self.body) self.assertEqual(headers, self.mac_00_header) + # Non-HTTPS + insecure_uri = 'http://example.com/path?query=world' + self.assertRaises(InsecureTransportError, client.add_token, insecure_uri, + body=self.body, + headers=self.headers, + issue_time=datetime.datetime.now()) + # Expired Token + expired = 523549800 + expired_token = { + 'expires_at': expired, + } + client = Client(self.client_id, token=expired_token, token_type="MAC", + access_token=self.access_token, mac_key=self.mac_key, + mac_algorithm="hmac-sha-1") + self.assertRaises(TokenExpiredError, client.add_token, self.uri, + body=self.body, + headers=self.headers, + issue_time=datetime.datetime.now()) - # Add the Authorization header (draft 00) + # Add the Authorization header (draft 01) client = Client(self.client_id, token_type="MAC", access_token=self.access_token, mac_key=self.mac_key, mac_algorithm="hmac-sha-1") @@ -160,7 +194,24 @@ def test_add_mac_token(self): self.assertEqual(uri, self.uri) self.assertEqual(body, self.body) self.assertEqual(headers, self.mac_01_header) - + # Non-HTTPS + insecure_uri = 'http://example.com/path?query=world' + self.assertRaises(InsecureTransportError, client.add_token, insecure_uri, + body=self.body, + headers=self.headers, + draft=1) + # Expired Token + expired = 523549800 + expired_token = { + 'expires_at': expired, + } + client = Client(self.client_id, token=expired_token, token_type="MAC", + access_token=self.access_token, mac_key=self.mac_key, + mac_algorithm="hmac-sha-1") + self.assertRaises(TokenExpiredError, client.add_token, self.uri, + body=self.body, + headers=self.headers, + draft=1) def test_revocation_request(self): client = Client(self.client_id) @@ -208,6 +259,21 @@ def test_prepare_authorization_request(self): # NotImplementedError self.assertRaises(NotImplementedError, client.prepare_authorization_request, auth_url) + def test_prepare_token_request(self): + redirect_url = 'https://example.com/callback/' + scopes = 'read' + token_url = 'https://example.com/token/' + state = 'fake_state' + + client = Client(self.client_id, scope=scopes, state=state) + + # Non-HTTPS + self.assertRaises(InsecureTransportError, + client.prepare_token_request, 'http://example.com/token/') + + # NotImplementedError + self.assertRaises(NotImplementedError, client.prepare_token_request, token_url) + def test_prepare_refresh_token_request(self): client = Client(self.client_id) diff --git a/tests/oauth2/rfc6749/clients/test_service_application.py b/tests/oauth2/rfc6749/clients/test_service_application.py index 2dc633a4..dc337cfa 100644 --- a/tests/oauth2/rfc6749/clients/test_service_application.py +++ b/tests/oauth2/rfc6749/clients/test_service_application.py @@ -89,8 +89,8 @@ def test_request_body(self, t): audience=self.audience, body=self.body) r = Request('https://a.b', body=body) - self.assertEqual(r.isnot, 'empty') - self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type) + self.assertEqual(r.isnot, 'empty') + self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type) claim = jwt.decode(r.assertion, self.public_key, audience=self.audience, algorithms=['RS256']) @@ -98,6 +98,72 @@ def test_request_body(self, t): # audience verification is handled during decode now self.assertEqual(claim['sub'], self.subject) self.assertEqual(claim['iat'], int(t.return_value)) + self.assertNotIn('nbf', claim) + self.assertNotIn('jti', claim) + + # Missing issuer parameter + self.assertRaises(ValueError, client.prepare_request_body, + issuer=None, subject=self.subject, audience=self.audience, body=self.body) + + # Missing subject parameter + self.assertRaises(ValueError, client.prepare_request_body, + issuer=self.issuer, subject=None, audience=self.audience, body=self.body) + + # Missing audience parameter + self.assertRaises(ValueError, client.prepare_request_body, + issuer=self.issuer, subject=self.subject, audience=None, body=self.body) + + # Optional kwargs + not_before = time() - 3600 + jwt_id = '8zd15df4s35f43sd' + body = client.prepare_request_body(issuer=self.issuer, + subject=self.subject, + audience=self.audience, + body=self.body, + not_before=not_before, + jwt_id=jwt_id) + + r = Request('https://a.b', body=body) + self.assertEqual(r.isnot, 'empty') + self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type) + + claim = jwt.decode(r.assertion, self.public_key, audience=self.audience, algorithms=['RS256']) + + self.assertEqual(claim['iss'], self.issuer) + # audience verification is handled during decode now + self.assertEqual(claim['sub'], self.subject) + self.assertEqual(claim['iat'], int(t.return_value)) + self.assertEqual(claim['nbf'], not_before) + self.assertEqual(claim['jti'], jwt_id) + + @patch('time.time') + def test_request_body_no_initial_private_key(self, t): + t.return_value = time() + self.token['expires_at'] = self.token['expires_in'] + t.return_value + + client = ServiceApplicationClient( + self.client_id, private_key=None) + + # Basic with private key provided + body = client.prepare_request_body(issuer=self.issuer, + subject=self.subject, + audience=self.audience, + body=self.body, + private_key=self.private_key) + r = Request('https://a.b', body=body) + self.assertEqual(r.isnot, 'empty') + self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type) + + claim = jwt.decode(r.assertion, self.public_key, audience=self.audience, algorithms=['RS256']) + + self.assertEqual(claim['iss'], self.issuer) + # audience verification is handled during decode now + self.assertEqual(claim['sub'], self.subject) + self.assertEqual(claim['iat'], int(t.return_value)) + + # No private key provided + self.assertRaises(ValueError, client.prepare_request_body, + issuer=self.issuer, subject=self.subject, audience=self.audience, body=self.body) @patch('time.time') def test_parse_token_response(self, t): From 6da09f284593546daac545d625f68014d7464c39 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 23 May 2018 16:59:22 +0200 Subject: [PATCH 042/237] Deploy only when building python36 with tox Avoid multiple deploy steps and lead to failures (e.g. errors "already deployed") --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0a7d8ad3..dd72d5cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,6 +34,7 @@ deploy: on: tags: true all_branches: true + condition: $TOXENV = py36 repo: oauthlib/oauthlib - provider: releases api_key: @@ -42,4 +43,5 @@ deploy: on: tags: true all_branches: true + condition: $TOXENV = py36 repo: oauthlib/oauthlib From 789220fc5b450ed72899d87961eef155fbd22fc6 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Mon, 23 Apr 2018 21:47:51 +0100 Subject: [PATCH 043/237] Prepare 2.1.0 release. (cherry picked from commit 5c76855) --- CHANGELOG.rst | 9 +++++++++ oauthlib/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7389af0f..a8e1941d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog ========= +2.1.0 (2018-05-21) +------------------ + +* Fixed some copy and paste typos (#535) +* Use secrets module in Python 3.6 and later (#533) +* Add request argument to confirm_redirect_uri (#504) +* Avoid populating spurious token credentials (#542) +* Make populate attributes API public (#546) + 2.0.7 (2018-03-19) ------------------ diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 36450108..3393efeb 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -10,7 +10,7 @@ """ __author__ = 'The OAuthlib Community' -__version__ = '2.0.7' +__version__ = '2.1.0' import logging From 27702f40753f88fc5bbf15128dac15758d4bc29a Mon Sep 17 00:00:00 2001 From: Mattia Procopio Date: Sat, 26 May 2018 21:33:41 +0200 Subject: [PATCH 044/237] Check that the Bearer header is properly formatted (#491) --- oauthlib/oauth2/rfc6749/tokens.py | 40 ++++++++------ tests/oauth2/rfc6749/test_tokens.py | 81 +++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 15 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index 4ae20e0e..a7491f47 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -220,6 +220,24 @@ def signed_token_generator(request): return signed_token_generator +def get_token_from_header(request): + """ + Helper function to extract a token from the request header. + :param request: The request object + :return: Return the token or None if the Authorization header is malformed. + """ + token = None + + if 'Authorization' in request.headers: + split_header = request.headers.get('Authorization').split() + if len(split_header) == 2 and split_header[0] == 'Bearer': + token = split_header[1] + else: + token = request.access_token + + return token + + class TokenBase(object): def __call__(self, request, refresh_token=False): @@ -286,16 +304,12 @@ def create_token(self, request, refresh_token=False, save_token=True): return token def validate_request(self, request): - token = None - if 'Authorization' in request.headers: - token = request.headers.get('Authorization')[7:] - else: - token = request.access_token + token = get_token_from_header(request) return self.request_validator.validate_bearer_token( token, request.scopes, request) def estimate_type(self, request): - if request.headers.get('Authorization', '').startswith('Bearer'): + if request.headers.get('Authorization', '').split(' ')[0] == 'Bearer': return 9 elif request.access_token is not None: return 5 @@ -331,17 +345,13 @@ def create_token(self, request, refresh_token=False, save_token=False): return self.request_validator.get_jwt_bearer_token(None, None, request) def validate_request(self, request): - token = None - if 'Authorization' in request.headers: - token = request.headers.get('Authorization')[7:] - else: - token = request.access_token + token = get_token_from_header(request) return self.request_validator.validate_jwt_bearer_token( token, request.scopes, request) def estimate_type(self, request): - token = request.headers.get('Authorization', '')[7:] - if token.startswith('ey') and token.count('.') in (2, 4): + split_header = request.headers.get('Authorization', '').split() + + if len(split_header) == 2 and split_header[0] == 'Bearer' and split_header[1].startswith('ey') and split_header[1].count('.') in (2, 4): return 10 - else: - return 0 + return 0 diff --git a/tests/oauth2/rfc6749/test_tokens.py b/tests/oauth2/rfc6749/test_tokens.py index 570afb01..ecac03e2 100644 --- a/tests/oauth2/rfc6749/test_tokens.py +++ b/tests/oauth2/rfc6749/test_tokens.py @@ -2,6 +2,7 @@ import mock +from oauthlib.common import Request from oauthlib.oauth2.rfc6749.tokens import * from ...unittest import TestCase @@ -61,9 +62,22 @@ class TokenTest(TestCase): bearer_headers = { 'Authorization': 'Bearer vF9dft4qmT' } + fake_bearer_headers = [ + {'Authorization': 'Beaver vF9dft4qmT'}, + {'Authorization': 'BeavervF9dft4qmT'}, + {'Authorization': 'Beaver vF9dft4qmT'}, + {'Authorization': 'BearerF9dft4qmT'}, + {'Authorization': 'Bearer vF9d ft4qmT'}, + ] + valid_header_with_multiple_spaces = {'Authorization': 'Bearer vF9dft4qmT'} bearer_body = 'access_token=vF9dft4qmT' bearer_uri = 'http://server.example.com/resource?access_token=vF9dft4qmT' + def _mocked_validate_bearer_token(self, token, scopes, request): + if not token: + return False + return True + def test_prepare_mac_header(self): """Verify mac signatures correctness @@ -83,8 +97,57 @@ def test_prepare_bearer_request(self): self.assertEqual(prepare_bearer_body(self.token), self.bearer_body) self.assertEqual(prepare_bearer_uri(self.token, uri=self.uri), self.bearer_uri) + def test_fake_bearer_is_not_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + + for fake_header in self.fake_bearer_headers: + request = Request('/', headers=fake_header) + result = BearerToken(request_validator=request_validator).validate_request(request) + + self.assertFalse(result) + + def test_header_with_multispaces_is_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + + request = Request('/', headers=self.valid_header_with_multiple_spaces) + result = BearerToken(request_validator=request_validator).validate_request(request) + + self.assertTrue(result) + + def test_estimate_type_with_fake_header_returns_type_0(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + + for fake_header in self.fake_bearer_headers: + request = Request('/', headers=fake_header) + result = BearerToken(request_validator=request_validator).estimate_type(request) + + if fake_header['Authorization'].count(' ') == 2 and \ + fake_header['Authorization'].split()[0] == 'Bearer': + # If we're dealing with the header containing 2 spaces, it will be recognized + # as a Bearer valid header, the token itself will be invalid by the way. + self.assertEqual(result, 9) + else: + self.assertEqual(result, 0) + class JWTTokenTestCase(TestCase): + fake_bearer_headers = [ + {'Authorization': 'Beaver vF9dft4qmT'}, + {'Authorization': 'BeavervF9dft4qmT'}, + {'Authorization': 'Beaver vF9dft4qmT'}, + {'Authorization': 'BearerF9dft4qmT'}, + {'Authorization': 'Bearer vF9df t4qmT'}, + ] + + valid_header_with_multiple_spaces = {'Authorization': 'Bearer vF9dft4qmT'} + + def _mocked_validate_bearer_token(self, token, scopes, request): + if not token: + return False + return True def test_create_token_callable_expires_in(self): """ @@ -180,6 +243,24 @@ def test_validate_token_from_request(self): request.scopes, request) + def test_fake_bearer_is_not_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_jwt_bearer_token = self._mocked_validate_bearer_token + + for fake_header in self.fake_bearer_headers: + request = Request('/', headers=fake_header) + result = JWTToken(request_validator=request_validator).validate_request(request) + + self.assertFalse(result) + + def test_header_with_multiple_spaces_is_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_jwt_bearer_token = self._mocked_validate_bearer_token + request = Request('/', headers=self.valid_header_with_multiple_spaces) + result = JWTToken(request_validator=request_validator).validate_request(request) + + self.assertTrue(result) + def test_estimate_type(self): """ Estimate type results for a jwt token From fedc1d1b740a0407ec59152750bbbd9dc736b51d Mon Sep 17 00:00:00 2001 From: Grey Li Date: Sun, 27 May 2018 03:38:05 +0800 Subject: [PATCH 045/237] Add missing NotImplementedError (#499) --- oauthlib/oauth2/rfc6749/clients/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 07ef8949..406832d7 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -143,6 +143,7 @@ def prepare_request_body(self, *args, **kwargs): def parse_request_uri_response(self, *args, **kwargs): """Abstract method used to parse redirection responses.""" + raise NotImplementedError("Must be implemented by inheriting classes.") def add_token(self, uri, http_method='GET', body=None, headers=None, token_placement=None, **kwargs): From a102731c88f496b557dedd4024fb9b82801d134a Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sun, 20 May 2018 07:42:31 +0300 Subject: [PATCH 046/237] Remove Python 2.6 compatibility code. --- oauthlib/__init__.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 3393efeb..b7586d2b 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -8,18 +8,10 @@ :copyright: (c) 2011 by Idan Gazit. :license: BSD, see LICENSE for details. """ +import logging +from logging import NullHandler __author__ = 'The OAuthlib Community' __version__ = '2.1.0' - -import logging -try: # Python 2.7+ - from logging import NullHandler -except ImportError: - class NullHandler(logging.Handler): - - def emit(self, record): - pass - logging.getLogger('oauthlib').addHandler(NullHandler()) From d5a4d5ea0eab04ddddefac7d1e7a4902fc469286 Mon Sep 17 00:00:00 2001 From: Wiliam Souza Date: Tue, 5 Jun 2018 11:33:21 -0300 Subject: [PATCH 047/237] OpenID Connect split (#525) * Add command to clean up builds to makefile * Fix docs strings for endpoints pre_configured * Chnage grant_types.openid_connect to include a deprecation warning be a backward compatible * Fix doc string for rfc6749.request_validator * Remove unused import * Change import to be explicity * Move JWTTokenTestCase to openid.connect.core.test_token * Move JWTToken to oauthlib.openid.connect.core.tokens * Move to openid connect test * Move openid connect exceptions to its own file * Remove openid connect from oauth2 server * Remove JWTToken from oauth tokens * Remove grant_types.openid_connect file * Add oauthlib/openid estructure and tests --- Makefile | 17 +- oauthlib/oauth2/__init__.py | 2 +- .../rfc6749/endpoints/pre_configured.py | 43 +- oauthlib/oauth2/rfc6749/errors.py | 123 +----- .../oauth2/rfc6749/grant_types/__init__.py | 8 - oauthlib/oauth2/rfc6749/request_validator.py | 4 +- oauthlib/oauth2/rfc6749/tokens.py | 40 -- oauthlib/openid/__init__.py | 0 oauthlib/openid/connect/__init__.py | 0 oauthlib/openid/connect/core/__init__.py | 0 .../connect/core/endpoints/pre_configured.py | 103 +++++ oauthlib/openid/connect/core/exceptions.py | 152 +++++++ .../connect/core/grant_types/__init__.py | 17 + .../core/grant_types/authorization_code.py | 24 ++ .../connect/core/grant_types/base.py} | 176 +------- .../connect/core/grant_types/dispatchers.py | 86 ++++ .../connect/core/grant_types/exceptions.py | 32 ++ .../openid/connect/core/grant_types/hybrid.py | 36 ++ .../connect/core/grant_types/implicit.py | 28 ++ .../openid/connect/core/request_validator.py | 188 ++++++++ oauthlib/openid/connect/core/tokens.py | 54 +++ .../grant_types/test_openid_connect.py | 403 ------------------ tests/oauth2/rfc6749/test_server.py | 99 +++-- tests/oauth2/rfc6749/test_tokens.py | 203 +-------- tests/openid/__init__.py | 0 tests/openid/connect/__init__.py | 0 tests/openid/connect/core/__init__.py | 0 .../core}/endpoints/test_claims_handling.py | 14 +- .../test_openid_connect_params_handling.py | 0 .../grant_types/test_authorization_code.py | 153 +++++++ .../core/grant_types/test_dispatchers.py | 125 ++++++ .../connect/core/grant_types/test_hybrid.py | 13 + .../connect/core/grant_types/test_implicit.py | 148 +++++++ .../connect/core/test_request_validator.py | 52 +++ tests/openid/connect/core/test_server.py | 178 ++++++++ tests/openid/connect/core/test_tokens.py | 133 ++++++ 36 files changed, 1641 insertions(+), 1013 deletions(-) create mode 100644 oauthlib/openid/__init__.py create mode 100644 oauthlib/openid/connect/__init__.py create mode 100644 oauthlib/openid/connect/core/__init__.py create mode 100644 oauthlib/openid/connect/core/endpoints/pre_configured.py create mode 100644 oauthlib/openid/connect/core/exceptions.py create mode 100644 oauthlib/openid/connect/core/grant_types/__init__.py create mode 100644 oauthlib/openid/connect/core/grant_types/authorization_code.py rename oauthlib/{oauth2/rfc6749/grant_types/openid_connect.py => openid/connect/core/grant_types/base.py} (65%) create mode 100644 oauthlib/openid/connect/core/grant_types/dispatchers.py create mode 100644 oauthlib/openid/connect/core/grant_types/exceptions.py create mode 100644 oauthlib/openid/connect/core/grant_types/hybrid.py create mode 100644 oauthlib/openid/connect/core/grant_types/implicit.py create mode 100644 oauthlib/openid/connect/core/request_validator.py create mode 100644 oauthlib/openid/connect/core/tokens.py delete mode 100644 tests/oauth2/rfc6749/grant_types/test_openid_connect.py create mode 100644 tests/openid/__init__.py create mode 100644 tests/openid/connect/__init__.py create mode 100644 tests/openid/connect/core/__init__.py rename tests/{oauth2/rfc6749 => openid/connect/core}/endpoints/test_claims_handling.py (86%) rename tests/{oauth2/rfc6749 => openid/connect/core}/endpoints/test_openid_connect_params_handling.py (100%) create mode 100644 tests/openid/connect/core/grant_types/test_authorization_code.py create mode 100644 tests/openid/connect/core/grant_types/test_dispatchers.py create mode 100644 tests/openid/connect/core/grant_types/test_hybrid.py create mode 100644 tests/openid/connect/core/grant_types/test_implicit.py create mode 100644 tests/openid/connect/core/test_request_validator.py create mode 100644 tests/openid/connect/core/test_server.py create mode 100644 tests/openid/connect/core/test_tokens.py diff --git a/Makefile b/Makefile index 8571a911..f9cc4abf 100644 --- a/Makefile +++ b/Makefile @@ -12,13 +12,27 @@ # Since these contacts will be addressed with Github mentions they # need to be Github users (for now)(sorry Bitbucket). # -clean: +clean: clean-eggs clean-build + @find . -iname '*.pyc' -delete + @find . -iname '*.pyo' -delete + @find . -iname '*~' -delete + @find . -iname '*.swp' -delete + @find . -iname '__pycache__' -delete rm -rf .tox rm -rf bottle-oauthlib rm -rf django-oauth-toolkit rm -rf flask-oauthlib rm -rf requests-oauthlib +clean-eggs: + @find . -name '*.egg' -print0|xargs -0 rm -rf -- + @rm -rf .eggs/ + +clean-build: + @rm -fr build/ + @rm -fr dist/ + @rm -fr *.egg-info + test: tox @@ -51,7 +65,6 @@ requests: cd requests-oauthlib 2>/dev/null || git clone https://github.com/requests/requests-oauthlib.git cd requests-oauthlib && sed -i.old 's,deps=,deps = --editable=file://{toxinidir}/../[signedtoken],' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox - .DEFAULT_GOAL := all .PHONY: clean test bottle django flask requests all: clean test bottle django flask requests diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index dc7b431c..303c6a1d 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -24,7 +24,7 @@ from .rfc6749.endpoints import MobileApplicationServer from .rfc6749.endpoints import LegacyApplicationServer from .rfc6749.endpoints import BackendApplicationServer -from .rfc6749.errors import AccessDeniedError, AccountSelectionRequired, ConsentRequired, FatalClientError, FatalOpenIDClientError, InsecureTransportError, InteractionRequired, InvalidClientError, InvalidClientIdError, InvalidGrantError, InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError, InvalidScopeError, LoginRequired, MismatchingRedirectURIError, MismatchingStateError, MissingClientIdError, MissingCodeError, MissingRedirectURIError, MissingResponseTypeError, MissingTokenError, MissingTokenTypeError, OAuth2Error, OpenIDClientError, ServerError, TemporarilyUnavailableError, TokenExpiredError, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError +from .rfc6749.errors import AccessDeniedError, OAuth2Error, FatalClientError, InsecureTransportError, InvalidClientError, InvalidClientIdError, InvalidGrantError, InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError, InvalidScopeError, MismatchingRedirectURIError, MismatchingStateError, MissingClientIdError, MissingCodeError, MissingRedirectURIError, MissingResponseTypeError, MissingTokenError, MissingTokenTypeError, ServerError, TemporarilyUnavailableError, TokenExpiredError, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError from .rfc6749.grant_types import AuthorizationCodeGrant from .rfc6749.grant_types import ImplicitGrant from .rfc6749.grant_types import ResourceOwnerPasswordCredentialsGrant diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py index 66af5161..e2cc9db7 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py +++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py @@ -1,22 +1,19 @@ # -*- coding: utf-8 -*- """ -oauthlib.oauth2.rfc6749 -~~~~~~~~~~~~~~~~~~~~~~~ +oauthlib.oauth2.rfc6749.endpoints.pre_configured +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This module is an implementation of various logic needed -for consuming and providing OAuth 2.0 RFC6749. +This module is an implementation of various endpoints needed +for providing OAuth 2.0 RFC6749 servers. """ from __future__ import absolute_import, unicode_literals -from ..grant_types import (AuthCodeGrantDispatcher, AuthorizationCodeGrant, - AuthTokenGrantDispatcher, +from ..grant_types import (AuthorizationCodeGrant, ClientCredentialsGrant, - ImplicitTokenGrantDispatcher, ImplicitGrant, - OpenIDConnectAuthCode, OpenIDConnectImplicit, - OpenIDConnectHybrid, + ImplicitGrant, RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant) -from ..tokens import BearerToken, JWTToken +from ..tokens import BearerToken from .authorization import AuthorizationEndpoint from .introspect import IntrospectEndpoint from .resource import ResourceEndpoint @@ -51,46 +48,28 @@ def __init__(self, request_validator, token_expires_in=None, request_validator) credentials_grant = ClientCredentialsGrant(request_validator) refresh_grant = RefreshTokenGrant(request_validator) - openid_connect_auth = OpenIDConnectAuthCode(request_validator) - openid_connect_implicit = OpenIDConnectImplicit(request_validator) - openid_connect_hybrid = OpenIDConnectHybrid(request_validator) bearer = BearerToken(request_validator, token_generator, token_expires_in, refresh_token_generator) - jwt = JWTToken(request_validator, token_generator, - token_expires_in, refresh_token_generator) - - auth_grant_choice = AuthCodeGrantDispatcher(default_auth_grant=auth_grant, oidc_auth_grant=openid_connect_auth) - implicit_grant_choice = ImplicitTokenGrantDispatcher(default_implicit_grant=implicit_grant, oidc_implicit_grant=openid_connect_implicit) - - # See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations - # internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination AuthorizationEndpoint.__init__(self, default_response_type='code', response_types={ - 'code': auth_grant_choice, - 'token': implicit_grant_choice, - 'id_token': openid_connect_implicit, - 'id_token token': openid_connect_implicit, - 'code token': openid_connect_hybrid, - 'code id_token': openid_connect_hybrid, - 'code id_token token': openid_connect_hybrid, + 'code': auth_grant, + 'token': implicit_grant, 'none': auth_grant }, default_token_type=bearer) - token_grant_choice = AuthTokenGrantDispatcher(request_validator, default_token_grant=auth_grant, oidc_token_grant=openid_connect_auth) - TokenEndpoint.__init__(self, default_grant_type='authorization_code', grant_types={ - 'authorization_code': token_grant_choice, + 'authorization_code': auth_grant, 'password': password_grant, 'client_credentials': credentials_grant, 'refresh_token': refresh_grant, }, default_token_type=bearer) ResourceEndpoint.__init__(self, default_token='Bearer', - token_types={'Bearer': bearer, 'JWT': jwt}) + token_types={'Bearer': bearer}) RevocationEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 1d5e98d5..5a0cca2b 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -274,106 +274,6 @@ class UnsupportedTokenTypeError(OAuth2Error): error = 'unsupported_token_type' -class FatalOpenIDClientError(FatalClientError): - pass - - -class OpenIDClientError(OAuth2Error): - pass - - -class InteractionRequired(OpenIDClientError): - """ - The Authorization Server requires End-User interaction to proceed. - - This error MAY be returned when the prompt parameter value in the - Authentication Request is none, but the Authentication Request cannot be - completed without displaying a user interface for End-User interaction. - """ - error = 'interaction_required' - status_code = 401 - - -class LoginRequired(OpenIDClientError): - """ - The Authorization Server requires End-User authentication. - - This error MAY be returned when the prompt parameter value in the - Authentication Request is none, but the Authentication Request cannot be - completed without displaying a user interface for End-User authentication. - """ - error = 'login_required' - status_code = 401 - - -class AccountSelectionRequired(OpenIDClientError): - """ - The End-User is REQUIRED to select a session at the Authorization Server. - - The End-User MAY be authenticated at the Authorization Server with - different associated accounts, but the End-User did not select a session. - This error MAY be returned when the prompt parameter value in the - Authentication Request is none, but the Authentication Request cannot be - completed without displaying a user interface to prompt for a session to - use. - """ - error = 'account_selection_required' - - -class ConsentRequired(OpenIDClientError): - """ - The Authorization Server requires End-User consent. - - This error MAY be returned when the prompt parameter value in the - Authentication Request is none, but the Authentication Request cannot be - completed without displaying a user interface for End-User consent. - """ - error = 'consent_required' - status_code = 401 - - -class InvalidRequestURI(OpenIDClientError): - """ - The request_uri in the Authorization Request returns an error or - contains invalid data. - """ - error = 'invalid_request_uri' - description = 'The request_uri in the Authorization Request returns an ' \ - 'error or contains invalid data.' - - -class InvalidRequestObject(OpenIDClientError): - """ - The request parameter contains an invalid Request Object. - """ - error = 'invalid_request_object' - description = 'The request parameter contains an invalid Request Object.' - - -class RequestNotSupported(OpenIDClientError): - """ - The OP does not support use of the request parameter. - """ - error = 'request_not_supported' - description = 'The request parameter is not supported.' - - -class RequestURINotSupported(OpenIDClientError): - """ - The OP does not support use of the request_uri parameter. - """ - error = 'request_uri_not_supported' - description = 'The request_uri parameter is not supported.' - - -class RegistrationNotSupported(OpenIDClientError): - """ - The OP does not support use of the registration parameter. - """ - error = 'registration_not_supported' - description = 'The registration parameter is not supported.' - - class InvalidTokenError(OAuth2Error): """ The access token provided is expired, revoked, malformed, or @@ -402,6 +302,29 @@ class InsufficientScopeError(OAuth2Error): "the access token.") +class ConsentRequired(OAuth2Error): + """ + The Authorization Server requires End-User consent. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User consent. + """ + error = 'consent_required' + status_code = 401 + +class LoginRequired(OAuth2Error): + """ + The Authorization Server requires End-User authentication. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User authentication. + """ + error = 'login_required' + status_code = 401 + + def raise_from_error(error, params=None): import inspect import sys diff --git a/oauthlib/oauth2/rfc6749/grant_types/__init__.py b/oauthlib/oauth2/rfc6749/grant_types/__init__.py index 2e4bfe4a..2ec8e4fa 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/__init__.py +++ b/oauthlib/oauth2/rfc6749/grant_types/__init__.py @@ -10,11 +10,3 @@ from .resource_owner_password_credentials import ResourceOwnerPasswordCredentialsGrant from .client_credentials import ClientCredentialsGrant from .refresh_token import RefreshTokenGrant -from .openid_connect import OpenIDConnectBase -from .openid_connect import OpenIDConnectAuthCode -from .openid_connect import OpenIDConnectImplicit -from .openid_connect import OpenIDConnectHybrid -from .openid_connect import OIDCNoPrompt -from .openid_connect import AuthCodeGrantDispatcher -from .openid_connect import AuthTokenGrantDispatcher -from .openid_connect import ImplicitTokenGrantDispatcher diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 56ecc3dd..92edba62 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -oauthlib.oauth2.rfc6749.grant_types -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +oauthlib.oauth2.rfc6749.request_validator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ """ from __future__ import absolute_import, unicode_literals diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index a7491f47..1d2b5eb4 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -315,43 +315,3 @@ def estimate_type(self, request): return 5 else: return 0 - - -class JWTToken(TokenBase): - __slots__ = ( - 'request_validator', 'token_generator', - 'refresh_token_generator', 'expires_in' - ) - - def __init__(self, request_validator=None, token_generator=None, - expires_in=None, refresh_token_generator=None): - self.request_validator = request_validator - self.token_generator = token_generator or random_token_generator - self.refresh_token_generator = ( - refresh_token_generator or self.token_generator - ) - self.expires_in = expires_in or 3600 - - def create_token(self, request, refresh_token=False, save_token=False): - """Create a JWT Token, using requestvalidator method.""" - - if callable(self.expires_in): - expires_in = self.expires_in(request) - else: - expires_in = self.expires_in - - request.expires_in = expires_in - - return self.request_validator.get_jwt_bearer_token(None, None, request) - - def validate_request(self, request): - token = get_token_from_header(request) - return self.request_validator.validate_jwt_bearer_token( - token, request.scopes, request) - - def estimate_type(self, request): - split_header = request.headers.get('Authorization', '').split() - - if len(split_header) == 2 and split_header[0] == 'Bearer' and split_header[1].startswith('ey') and split_header[1].count('.') in (2, 4): - return 10 - return 0 diff --git a/oauthlib/openid/__init__.py b/oauthlib/openid/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/oauthlib/openid/connect/__init__.py b/oauthlib/openid/connect/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/oauthlib/openid/connect/core/__init__.py b/oauthlib/openid/connect/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py new file mode 100644 index 00000000..3bcd24de --- /dev/null +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.endpoints.pre_configured +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various endpoints needed +for providing OpenID Connect servers. +""" +from __future__ import absolute_import, unicode_literals + +from ..grant_types import ( + AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant, + ClientCredentialsGrant, + ImplicitGrant as OAuth2ImplicitGrant, + RefreshTokenGrant, + ResourceOwnerPasswordCredentialsGrant +) + +from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant +from oauthlib.openid.connect.core.grant_types.dispatchers import ( + AuthorizationCodeGrantDispatcher, + ImplicitTokenGrantDispatcher, + AuthorizationTokenGrantDispatcher +) +from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant +from oauthlib.openid.connect.core.tokens import JWTToken + +from ..tokens import BearerToken +from .authorization import AuthorizationEndpoint +from .resource import ResourceEndpoint +from .revocation import RevocationEndpoint +from .token import TokenEndpoint + + +class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, + RevocationEndpoint): + + """An all-in-one endpoint featuring all four major grant types.""" + + def __init__(self, request_validator, token_expires_in=None, + token_generator=None, refresh_token_generator=None, + *args, **kwargs): + """Construct a new all-grants-in-one server. + + :param request_validator: An implementation of + oauthlib.oauth2.RequestValidator. + :param token_expires_in: An int or a function to generate a token + expiration offset (in seconds) given a + oauthlib.common.Request object. + :param token_generator: A function to generate a token from a request. + :param refresh_token_generator: A function to generate a token from a + request for the refresh token. + :param kwargs: Extra parameters to pass to authorization-, + token-, resource-, and revocation-endpoint constructors. + """ + auth_grant = OAuth2AuthorizationCodeGrant(request_validator) + implicit_grant = OAuth2ImplicitGrant(request_validator) + password_grant = ResourceOwnerPasswordCredentialsGrant( + request_validator) + credentials_grant = ClientCredentialsGrant(request_validator) + refresh_grant = RefreshTokenGrant(request_validator) + openid_connect_auth = AuthorizationCodeGrant(request_validator) + openid_connect_implicit = ImplicitGrant(request_validator) + openid_connect_hybrid = HybridGrant(request_validator) + + bearer = BearerToken(request_validator, token_generator, + token_expires_in, refresh_token_generator) + + jwt = JWTToken(request_validator, token_generator, + token_expires_in, refresh_token_generator) + + auth_grant_choice = AuthorizationCodeGrantDispatcher(default_auth_grant=auth_grant, oidc_auth_grant=openid_connect_auth) + implicit_grant_choice = ImplicitTokenGrantDispatcher(default_implicit_grant=implicit_grant, oidc_implicit_grant=openid_connect_implicit) + + # See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations + # internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination + AuthorizationEndpoint.__init__(self, default_response_type='code', + response_types={ + 'code': auth_grant_choice, + 'token': implicit_grant_choice, + 'id_token': openid_connect_implicit, + 'id_token token': openid_connect_implicit, + 'code token': openid_connect_hybrid, + 'code id_token': openid_connect_hybrid, + 'code id_token token': openid_connect_hybrid, + 'none': auth_grant + }, + default_token_type=bearer) + + token_grant_choice = AuthorizationTokenGrantDispatcher(request_validator, default_token_grant=auth_grant, oidc_token_grant=openid_connect_auth) + + TokenEndpoint.__init__(self, default_grant_type='authorization_code', + grant_types={ + 'authorization_code': token_grant_choice, + 'password': password_grant, + 'client_credentials': credentials_grant, + 'refresh_token': refresh_grant, + }, + default_token_type=bearer) + ResourceEndpoint.__init__(self, default_token='Bearer', + token_types={'Bearer': bearer, 'JWT': jwt}) + RevocationEndpoint.__init__(self, request_validator) diff --git a/oauthlib/openid/connect/core/exceptions.py b/oauthlib/openid/connect/core/exceptions.py new file mode 100644 index 00000000..8b08d21f --- /dev/null +++ b/oauthlib/openid/connect/core/exceptions.py @@ -0,0 +1,152 @@ +# coding=utf-8 +""" +oauthlib.oauth2.rfc6749.errors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Error used both by OAuth 2 clients and providers to represent the spec +defined error responses for all four core grant types. +""" +from __future__ import unicode_literals + +from oauthlib.oauth2.rfc6749.errors import FatalClientError, OAuth2Error + + +class FatalOpenIDClientError(FatalClientError): + pass + + +class OpenIDClientError(OAuth2Error): + pass + + +class InteractionRequired(OpenIDClientError): + """ + The Authorization Server requires End-User interaction to proceed. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User interaction. + """ + error = 'interaction_required' + status_code = 401 + + +class LoginRequired(OpenIDClientError): + """ + The Authorization Server requires End-User authentication. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User authentication. + """ + error = 'login_required' + status_code = 401 + + +class AccountSelectionRequired(OpenIDClientError): + """ + The End-User is REQUIRED to select a session at the Authorization Server. + + The End-User MAY be authenticated at the Authorization Server with + different associated accounts, but the End-User did not select a session. + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface to prompt for a session to + use. + """ + error = 'account_selection_required' + + +class ConsentRequired(OpenIDClientError): + """ + The Authorization Server requires End-User consent. + + This error MAY be returned when the prompt parameter value in the + Authentication Request is none, but the Authentication Request cannot be + completed without displaying a user interface for End-User consent. + """ + error = 'consent_required' + status_code = 401 + + +class InvalidRequestURI(OpenIDClientError): + """ + The request_uri in the Authorization Request returns an error or + contains invalid data. + """ + error = 'invalid_request_uri' + description = 'The request_uri in the Authorization Request returns an ' \ + 'error or contains invalid data.' + + +class InvalidRequestObject(OpenIDClientError): + """ + The request parameter contains an invalid Request Object. + """ + error = 'invalid_request_object' + description = 'The request parameter contains an invalid Request Object.' + + +class RequestNotSupported(OpenIDClientError): + """ + The OP does not support use of the request parameter. + """ + error = 'request_not_supported' + description = 'The request parameter is not supported.' + + +class RequestURINotSupported(OpenIDClientError): + """ + The OP does not support use of the request_uri parameter. + """ + error = 'request_uri_not_supported' + description = 'The request_uri parameter is not supported.' + + +class RegistrationNotSupported(OpenIDClientError): + """ + The OP does not support use of the registration parameter. + """ + error = 'registration_not_supported' + description = 'The registration parameter is not supported.' + + +class InvalidTokenError(OAuth2Error): + """ + The access token provided is expired, revoked, malformed, or + invalid for other reasons. The resource SHOULD respond with + the HTTP 401 (Unauthorized) status code. The client MAY + request a new access token and retry the protected resource + request. + """ + error = 'invalid_token' + status_code = 401 + description = ("The access token provided is expired, revoked, malformed, " + "or invalid for other reasons.") + + +class InsufficientScopeError(OAuth2Error): + """ + The request requires higher privileges than provided by the + access token. The resource server SHOULD respond with the HTTP + 403 (Forbidden) status code and MAY include the "scope" + attribute with the scope necessary to access the protected + resource. + """ + error = 'insufficient_scope' + status_code = 403 + description = ("The request requires higher privileges than provided by " + "the access token.") + + +def raise_from_error(error, params=None): + import inspect + import sys + kwargs = { + 'description': params.get('error_description'), + 'uri': params.get('error_uri'), + 'state': params.get('state') + } + for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): + if cls.error == error: + raise cls(**kwargs) diff --git a/oauthlib/openid/connect/core/grant_types/__init__.py b/oauthlib/openid/connect/core/grant_types/__init__.py new file mode 100644 index 00000000..7fc183d6 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import unicode_literals, absolute_import + +from .authorization_code import AuthorizationCodeGrant +from .implicit import ImplicitGrant +from .base import GrantTypeBase +from .hybrid import HybridGrant +from .exceptions import OIDCNoPrompt +from oauthlib.openid.connect.core.grant_types.dispatchers import ( + AuthorizationCodeGrantDispatcher, + ImplicitTokenGrantDispatcher, + AuthorizationTokenGrantDispatcher +) diff --git a/oauthlib/openid/connect/core/grant_types/authorization_code.py b/oauthlib/openid/connect/core/grant_types/authorization_code.py new file mode 100644 index 00000000..b0b10155 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/authorization_code.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from oauthlib.oauth2.rfc6749.grant_types.authorization_code import AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant + +from .base import GrantTypeBase + +log = logging.getLogger(__name__) + + +class AuthorizationCodeGrant(GrantTypeBase): + + def __init__(self, request_validator=None, **kwargs): + self.proxy_target = OAuth2AuthorizationCodeGrant( + request_validator=request_validator, **kwargs) + self.custom_validators.post_auth.append( + self.openid_authorization_validator) + self.register_token_modifier(self.add_id_token) diff --git a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py b/oauthlib/openid/connect/core/grant_types/base.py similarity index 65% rename from oauthlib/oauth2/rfc6749/grant_types/openid_connect.py rename to oauthlib/openid/connect/core/grant_types/base.py index 4371b286..2bb48b1e 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -1,141 +1,15 @@ -# -*- coding: utf-8 -*- -""" -oauthlib.oauth2.rfc6749.grant_types.openid_connect -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -""" -from __future__ import absolute_import, unicode_literals +from .exceptions import OIDCNoPrompt import datetime import logging from json import loads -from ..errors import ConsentRequired, InvalidRequestError, LoginRequired -from ..request_validator import RequestValidator -from .authorization_code import AuthorizationCodeGrant -from .implicit import ImplicitGrant +from oauthlib.oauth2.rfc6749.errors import ConsentRequired, InvalidRequestError, LoginRequired log = logging.getLogger(__name__) -class OIDCNoPrompt(Exception): - """Exception used to inform users that no explicit authorization is needed. - - Normally users authorize requests after validation of the request is done. - Then post-authorization validation is again made and a response containing - an auth code or token is created. However, when OIDC clients request - no prompting of user authorization the final response is created directly. - - Example (without the shortcut for no prompt) - - scopes, req_info = endpoint.validate_authorization_request(url, ...) - authorization_view = create_fancy_auth_form(scopes, req_info) - return authorization_view - - Example (with the no prompt shortcut) - try: - scopes, req_info = endpoint.validate_authorization_request(url, ...) - authorization_view = create_fancy_auth_form(scopes, req_info) - return authorization_view - except OIDCNoPrompt: - # Note: Location will be set for you - headers, body, status = endpoint.create_authorization_response(url, ...) - redirect_view = create_redirect(headers, body, status) - return redirect_view - """ - - def __init__(self): - msg = ("OIDC request for no user interaction received. Do not ask user " - "for authorization, it should been done using silent " - "authentication through create_authorization_response. " - "See OIDCNoPrompt.__doc__ for more details.") - super(OIDCNoPrompt, self).__init__(msg) - - -class AuthCodeGrantDispatcher(object): - """ - This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope - including 'openid' to either the default_auth_grant or the oidc_auth_grant based on the scopes requested. - """ - def __init__(self, default_auth_grant=None, oidc_auth_grant=None): - self.default_auth_grant = default_auth_grant - self.oidc_auth_grant = oidc_auth_grant - - def _handler_for_request(self, request): - handler = self.default_auth_grant - - if request.scopes and "openid" in request.scopes: - handler = self.oidc_auth_grant - - log.debug('Selecting handler for request %r.', handler) - return handler - - def create_authorization_response(self, request, token_handler): - return self._handler_for_request(request).create_authorization_response(request, token_handler) - - def validate_authorization_request(self, request): - return self._handler_for_request(request).validate_authorization_request(request) - - -class ImplicitTokenGrantDispatcher(object): - """ - This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope - including 'openid' to either the default_auth_grant or the oidc_auth_grant based on the scopes requested. - """ - def __init__(self, default_implicit_grant=None, oidc_implicit_grant=None): - self.default_implicit_grant = default_implicit_grant - self.oidc_implicit_grant = oidc_implicit_grant - - def _handler_for_request(self, request): - handler = self.default_implicit_grant - - if request.scopes and "openid" in request.scopes and 'id_token' in request.response_type: - handler = self.oidc_implicit_grant - - log.debug('Selecting handler for request %r.', handler) - return handler - - def create_authorization_response(self, request, token_handler): - return self._handler_for_request(request).create_authorization_response(request, token_handler) - - def validate_authorization_request(self, request): - return self._handler_for_request(request).validate_authorization_request(request) - - -class AuthTokenGrantDispatcher(object): - """ - This is an adapter class that will route simple Token requests, those that authorization_code have a scope - including 'openid' to either the default_token_grant or the oidc_token_grant based on the scopes requested. - """ - def __init__(self, request_validator, default_token_grant=None, oidc_token_grant=None): - self.default_token_grant = default_token_grant - self.oidc_token_grant = oidc_token_grant - self.request_validator = request_validator - - def _handler_for_request(self, request): - handler = self.default_token_grant - scopes = () - parameters = dict(request.decoded_body) - client_id = parameters.get('client_id', None) - code = parameters.get('code', None) - redirect_uri = parameters.get('redirect_uri', None) - - # If code is not pressent fallback to `default_token_grant` wich will - # raise an error for the missing `code` in `create_token_response` step. - if code: - scopes = self.request_validator.get_authorization_code_scopes(client_id, code, redirect_uri, request) - - if 'openid' in scopes: - handler = self.oidc_token_grant - - log.debug('Selecting handler for request %r.', handler) - return handler - - def create_token_response(self, request, token_handler): - handler = self._handler_for_request(request) - return handler.create_token_response(request, token_handler) - - -class OpenIDConnectBase(object): +class GrantTypeBase(object): # Just proxy the majority of method calls through to the # proxy_target grant type handler, which will usually be either @@ -406,46 +280,4 @@ def openid_implicit_authorization_validator(self, request): return {} -class OpenIDConnectAuthCode(OpenIDConnectBase): - - def __init__(self, request_validator=None, **kwargs): - self.proxy_target = AuthorizationCodeGrant( - request_validator=request_validator, **kwargs) - self.custom_validators.post_auth.append( - self.openid_authorization_validator) - self.register_token_modifier(self.add_id_token) - - -class OpenIDConnectImplicit(OpenIDConnectBase): - - def __init__(self, request_validator=None, **kwargs): - self.proxy_target = ImplicitGrant( - request_validator=request_validator, **kwargs) - self.register_response_type('id_token') - self.register_response_type('id_token token') - self.custom_validators.post_auth.append( - self.openid_authorization_validator) - self.custom_validators.post_auth.append( - self.openid_implicit_authorization_validator) - self.register_token_modifier(self.add_id_token) - - -class OpenIDConnectHybrid(OpenIDConnectBase): - - def __init__(self, request_validator=None, **kwargs): - self.request_validator = request_validator or RequestValidator() - - self.proxy_target = AuthorizationCodeGrant( - request_validator=request_validator, **kwargs) - # All hybrid response types should be fragment-encoded. - self.proxy_target.default_response_mode = "fragment" - self.register_response_type('code id_token') - self.register_response_type('code token') - self.register_response_type('code id_token token') - self.custom_validators.post_auth.append( - self.openid_authorization_validator) - # Hybrid flows can return the id_token from the authorization - # endpoint as part of the 'code' response - self.register_code_modifier(self.add_token) - self.register_code_modifier(self.add_id_token) - self.register_token_modifier(self.add_id_token) +OpenIDConnectBase = GrantTypeBase diff --git a/oauthlib/openid/connect/core/grant_types/dispatchers.py b/oauthlib/openid/connect/core/grant_types/dispatchers.py new file mode 100644 index 00000000..2c334063 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/dispatchers.py @@ -0,0 +1,86 @@ +import logging +log = logging.getLogger(__name__) + + +class AuthorizationCodeGrantDispatcher(object): + """ + This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope + including 'openid' to either the default_auth_grant or the oidc_auth_grant based on the scopes requested. + """ + def __init__(self, default_auth_grant=None, oidc_auth_grant=None): + self.default_auth_grant = default_auth_grant + self.oidc_auth_grant = oidc_auth_grant + + def _handler_for_request(self, request): + handler = self.default_auth_grant + + if request.scopes and "openid" in request.scopes: + handler = self.oidc_auth_grant + + log.debug('Selecting handler for request %r.', handler) + return handler + + def create_authorization_response(self, request, token_handler): + return self._handler_for_request(request).create_authorization_response(request, token_handler) + + def validate_authorization_request(self, request): + return self._handler_for_request(request).validate_authorization_request(request) + + +class ImplicitTokenGrantDispatcher(object): + """ + This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope + including 'openid' to either the default_auth_grant or the oidc_auth_grant based on the scopes requested. + """ + def __init__(self, default_implicit_grant=None, oidc_implicit_grant=None): + self.default_implicit_grant = default_implicit_grant + self.oidc_implicit_grant = oidc_implicit_grant + + def _handler_for_request(self, request): + handler = self.default_implicit_grant + + if request.scopes and "openid" in request.scopes and 'id_token' in request.response_type: + handler = self.oidc_implicit_grant + + log.debug('Selecting handler for request %r.', handler) + return handler + + def create_authorization_response(self, request, token_handler): + return self._handler_for_request(request).create_authorization_response(request, token_handler) + + def validate_authorization_request(self, request): + return self._handler_for_request(request).validate_authorization_request(request) + + +class AuthorizationTokenGrantDispatcher(object): + """ + This is an adapter class that will route simple Token requests, those that authorization_code have a scope + including 'openid' to either the default_token_grant or the oidc_token_grant based on the scopes requested. + """ + def __init__(self, request_validator, default_token_grant=None, oidc_token_grant=None): + self.default_token_grant = default_token_grant + self.oidc_token_grant = oidc_token_grant + self.request_validator = request_validator + + def _handler_for_request(self, request): + handler = self.default_token_grant + scopes = () + parameters = dict(request.decoded_body) + client_id = parameters.get('client_id', None) + code = parameters.get('code', None) + redirect_uri = parameters.get('redirect_uri', None) + + # If code is not pressent fallback to `default_token_grant` wich will + # raise an error for the missing `code` in `create_token_response` step. + if code: + scopes = self.request_validator.get_authorization_code_scopes(client_id, code, redirect_uri, request) + + if 'openid' in scopes: + handler = self.oidc_token_grant + + log.debug('Selecting handler for request %r.', handler) + return handler + + def create_token_response(self, request, token_handler): + handler = self._handler_for_request(request) + return handler.create_token_response(request, token_handler) diff --git a/oauthlib/openid/connect/core/grant_types/exceptions.py b/oauthlib/openid/connect/core/grant_types/exceptions.py new file mode 100644 index 00000000..809f1b3b --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/exceptions.py @@ -0,0 +1,32 @@ +class OIDCNoPrompt(Exception): + """Exception used to inform users that no explicit authorization is needed. + + Normally users authorize requests after validation of the request is done. + Then post-authorization validation is again made and a response containing + an auth code or token is created. However, when OIDC clients request + no prompting of user authorization the final response is created directly. + + Example (without the shortcut for no prompt) + + scopes, req_info = endpoint.validate_authorization_request(url, ...) + authorization_view = create_fancy_auth_form(scopes, req_info) + return authorization_view + + Example (with the no prompt shortcut) + try: + scopes, req_info = endpoint.validate_authorization_request(url, ...) + authorization_view = create_fancy_auth_form(scopes, req_info) + return authorization_view + except OIDCNoPrompt: + # Note: Location will be set for you + headers, body, status = endpoint.create_authorization_response(url, ...) + redirect_view = create_redirect(headers, body, status) + return redirect_view + """ + + def __init__(self): + msg = ("OIDC request for no user interaction received. Do not ask user " + "for authorization, it should been done using silent " + "authentication through create_authorization_response. " + "See OIDCNoPrompt.__doc__ for more details.") + super(OIDCNoPrompt, self).__init__(msg) diff --git a/oauthlib/openid/connect/core/grant_types/hybrid.py b/oauthlib/openid/connect/core/grant_types/hybrid.py new file mode 100644 index 00000000..54669ae4 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/hybrid.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from oauthlib.oauth2.rfc6749.grant_types.authorization_code import AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant + +from .base import GrantTypeBase +from ..request_validator import RequestValidator + +log = logging.getLogger(__name__) + + +class HybridGrant(GrantTypeBase): + + def __init__(self, request_validator=None, **kwargs): + self.request_validator = request_validator or RequestValidator() + + self.proxy_target = OAuth2AuthorizationCodeGrant( + request_validator=request_validator, **kwargs) + # All hybrid response types should be fragment-encoded. + self.proxy_target.default_response_mode = "fragment" + self.register_response_type('code id_token') + self.register_response_type('code token') + self.register_response_type('code id_token token') + self.custom_validators.post_auth.append( + self.openid_authorization_validator) + # Hybrid flows can return the id_token from the authorization + # endpoint as part of the 'code' response + self.register_code_modifier(self.add_token) + self.register_code_modifier(self.add_id_token) + self.register_token_modifier(self.add_id_token) diff --git a/oauthlib/openid/connect/core/grant_types/implicit.py b/oauthlib/openid/connect/core/grant_types/implicit.py new file mode 100644 index 00000000..0eaa5b38 --- /dev/null +++ b/oauthlib/openid/connect/core/grant_types/implicit.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from .base import GrantTypeBase + +from oauthlib.oauth2.rfc6749.grant_types.implicit import ImplicitGrant as OAuth2ImplicitGrant + +log = logging.getLogger(__name__) + + +class ImplicitGrant(GrantTypeBase): + + def __init__(self, request_validator=None, **kwargs): + self.proxy_target = OAuth2ImplicitGrant( + request_validator=request_validator, **kwargs) + self.register_response_type('id_token') + self.register_response_type('id_token token') + self.custom_validators.post_auth.append( + self.openid_authorization_validator) + self.custom_validators.post_auth.append( + self.openid_implicit_authorization_validator) + self.register_token_modifier(self.add_id_token) diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py new file mode 100644 index 00000000..f3bcbdb5 --- /dev/null +++ b/oauthlib/openid/connect/core/request_validator.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid.connect.core.request_validator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from oauthlib.oauth2.rfc6749.request_validator import RequestValidator as OAuth2RequestValidator + +log = logging.getLogger(__name__) + + +class RequestValidator(OAuth2RequestValidator): + + def get_authorization_code_scopes(self, client_id, code, redirect_uri, request): + """ Extracts scopes from saved authorization code. + + The scopes returned by this method is used to route token requests + based on scopes passed to Authorization Code requests. + + With that the token endpoint knows when to include OpenIDConnect + id_token in token response only based on authorization code scopes. + + Only code param should be sufficient to retrieve grant code from + any storage you are using, `client_id` and `redirect_uri` can gave a + blank value `""` don't forget to check it before using those values + in a select query if a database is used. + + :param client_id: Unicode client identifier + :param code: Unicode authorization code grant + :param redirect_uri: Unicode absolute URI + :return: A list of scope + + Method is used by: + - Authorization Token Grant Dispatcher + """ + raise NotImplementedError('Subclasses must implement this method.') + + def get_jwt_bearer_token(self, token, token_handler, request): + """Get JWT Bearer token or OpenID Connect ID token + + If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token` + + :param token: A Bearer token dict + :param token_handler: the token handler (BearerToken class) + :param request: the HTTP Request (oauthlib.common.Request) + :return: The JWT Bearer token or OpenID Connect ID token (a JWS signed JWT) + + Method is used by JWT Bearer and OpenID Connect tokens: + - JWTToken.create_token + """ + raise NotImplementedError('Subclasses must implement this method.') + + def get_id_token(self, token, token_handler, request): + """Get OpenID Connect ID token + + In the OpenID Connect workflows when an ID Token is requested this method is called. + Subclasses should implement the construction, signing and optional encryption of the + ID Token as described in the OpenID Connect spec. + + In addition to the standard OAuth2 request properties, the request may also contain + these OIDC specific properties which are useful to this method: + + - nonce, if workflow is implicit or hybrid and it was provided + - claims, if provided to the original Authorization Code request + + The token parameter is a dict which may contain an ``access_token`` entry, in which + case the resulting ID Token *should* include a calculated ``at_hash`` claim. + + Similarly, when the request parameter has a ``code`` property defined, the ID Token + *should* include a calculated ``c_hash`` claim. + + http://openid.net/specs/openid-connect-core-1_0.html (sections `3.1.3.6`_, `3.2.2.10`_, `3.3.2.11`_) + + .. _`3.1.3.6`: http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken + .. _`3.2.2.10`: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken + .. _`3.3.2.11`: http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken + + :param token: A Bearer token dict + :param token_handler: the token handler (BearerToken class) + :param request: the HTTP Request (oauthlib.common.Request) + :return: The ID Token (a JWS signed JWT) + """ + # the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token + raise NotImplementedError('Subclasses must implement this method.') + + def validate_jwt_bearer_token(self, token, scopes, request): + """Ensure the JWT Bearer token or OpenID Connect ID token are valids and authorized access to scopes. + + If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token` + + If not using OpenID Connect this can `return None` to avoid 5xx rather 401/3 response. + + OpenID connect core 1.0 describe how to validate an id_token: + - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2 + + :param token: Unicode Bearer token + :param scopes: List of scopes (defined by you) + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is indirectly used by all core OpenID connect JWT token issuing grant types: + - Authorization Code Grant + - Implicit Grant + - Hybrid Grant + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_id_token(self, token, scopes, request): + """Ensure the id token is valid and authorized access to scopes. + + OpenID connect core 1.0 describe how to validate an id_token: + - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation + - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2 + + :param token: Unicode Bearer token + :param scopes: List of scopes (defined by you) + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is indirectly used by all core OpenID connect JWT token issuing grant types: + - Authorization Code Grant + - Implicit Grant + - Hybrid Grant + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_silent_authorization(self, request): + """Ensure the logged in user has authorized silent OpenID authorization. + + Silent OpenID authorization allows access tokens and id tokens to be + granted to clients without any user prompt or interaction. + + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - OpenIDConnectAuthCode + - OpenIDConnectImplicit + - OpenIDConnectHybrid + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_silent_login(self, request): + """Ensure session user has authorized silent OpenID login. + + If no user is logged in or has not authorized silent login, this + method should return False. + + If the user is logged in but associated with multiple accounts and + not selected which one to link to the token then this method should + raise an oauthlib.oauth2.AccountSelectionRequired error. + + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - OpenIDConnectAuthCode + - OpenIDConnectImplicit + - OpenIDConnectHybrid + """ + raise NotImplementedError('Subclasses must implement this method.') + + def validate_user_match(self, id_token_hint, scopes, claims, request): + """Ensure client supplied user id hint matches session user. + + If the sub claim or id_token_hint is supplied then the session + user must match the given ID. + + :param id_token_hint: User identifier string. + :param scopes: List of OAuth 2 scopes and OpenID claims (strings). + :param claims: OpenID Connect claims dict. + :param request: The HTTP Request (oauthlib.common.Request) + :rtype: True or False + + Method is used by: + - OpenIDConnectAuthCode + - OpenIDConnectImplicit + - OpenIDConnectHybrid + """ + raise NotImplementedError('Subclasses must implement this method.') diff --git a/oauthlib/openid/connect/core/tokens.py b/oauthlib/openid/connect/core/tokens.py new file mode 100644 index 00000000..6b68891a --- /dev/null +++ b/oauthlib/openid/connect/core/tokens.py @@ -0,0 +1,54 @@ +""" +authlib.openid.connect.core.tokens +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module contains methods for adding JWT tokens to requests. +""" +from __future__ import absolute_import, unicode_literals + + +from oauthlib.oauth2.rfc6749.tokens import TokenBase, random_token_generator + + +class JWTToken(TokenBase): + __slots__ = ( + 'request_validator', 'token_generator', + 'refresh_token_generator', 'expires_in' + ) + + def __init__(self, request_validator=None, token_generator=None, + expires_in=None, refresh_token_generator=None): + self.request_validator = request_validator + self.token_generator = token_generator or random_token_generator + self.refresh_token_generator = ( + refresh_token_generator or self.token_generator + ) + self.expires_in = expires_in or 3600 + + def create_token(self, request, refresh_token=False, save_token=False): + """Create a JWT Token, using requestvalidator method.""" + + if callable(self.expires_in): + expires_in = self.expires_in(request) + else: + expires_in = self.expires_in + + request.expires_in = expires_in + + return self.request_validator.get_jwt_bearer_token(None, None, request) + + def validate_request(self, request): + token = None + if 'Authorization' in request.headers: + token = request.headers.get('Authorization')[7:] + else: + token = request.access_token + return self.request_validator.validate_jwt_bearer_token( + token, request.scopes, request) + + def estimate_type(self, request): + token = request.headers.get('Authorization', '')[7:] + if token.startswith('ey') and token.count('.') in (2, 4): + return 10 + else: + return 0 diff --git a/tests/oauth2/rfc6749/grant_types/test_openid_connect.py b/tests/oauth2/rfc6749/grant_types/test_openid_connect.py deleted file mode 100644 index 573d491d..00000000 --- a/tests/oauth2/rfc6749/grant_types/test_openid_connect.py +++ /dev/null @@ -1,403 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import json - -import mock - -from oauthlib.common import Request -from oauthlib.oauth2.rfc6749.grant_types import (AuthTokenGrantDispatcher, - AuthorizationCodeGrant, - ImplicitGrant, - ImplicitTokenGrantDispatcher, - OIDCNoPrompt, - OpenIDConnectAuthCode, - OpenIDConnectHybrid, - OpenIDConnectImplicit) -from oauthlib.oauth2.rfc6749.tokens import BearerToken - -from ....unittest import TestCase -from .test_authorization_code import AuthorizationCodeGrantTest -from .test_implicit import ImplicitGrantTest - - -class OpenIDAuthCodeInterferenceTest(AuthorizationCodeGrantTest): - """Test that OpenID don't interfere with normal OAuth 2 flows.""" - - def setUp(self): - super(OpenIDAuthCodeInterferenceTest, self).setUp() - self.auth = OpenIDConnectAuthCode(request_validator=self.mock_validator) - - -class OpenIDImplicitInterferenceTest(ImplicitGrantTest): - """Test that OpenID don't interfere with normal OAuth 2 flows.""" - - def setUp(self): - super(OpenIDImplicitInterferenceTest, self).setUp() - self.auth = OpenIDConnectImplicit(request_validator=self.mock_validator) - - -class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): - """Test that OpenID don't interfere with normal OAuth 2 flows.""" - - def setUp(self): - super(OpenIDHybridInterferenceTest, self).setUp() - self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator) - - -def get_id_token_mock(token, token_handler, request): - return "MOCKED_TOKEN" - - -class OpenIDAuthCodeTest(TestCase): - - def setUp(self): - self.request = Request('http://a.b/path') - self.request.scopes = ('hello', 'openid') - self.request.expires_in = 1800 - self.request.client_id = 'abcdef' - self.request.code = '1234' - self.request.response_type = 'code' - self.request.grant_type = 'authorization_code' - self.request.redirect_uri = 'https://a.b/cb' - self.request.state = 'abc' - - self.mock_validator = mock.MagicMock() - self.mock_validator.authenticate_client.side_effect = self.set_client - self.mock_validator.get_id_token.side_effect = get_id_token_mock - self.auth = OpenIDConnectAuthCode(request_validator=self.mock_validator) - - self.url_query = 'https://a.b/cb?code=abc&state=abc' - self.url_fragment = 'https://a.b/cb#code=abc&state=abc' - - def set_client(self, request): - request.client = mock.MagicMock() - request.client.client_id = 'mocked' - return True - - @mock.patch('oauthlib.common.generate_token') - def test_authorization(self, generate_token): - - scope, info = self.auth.validate_authorization_request(self.request) - - generate_token.return_value = 'abc' - bearer = BearerToken(self.mock_validator) - self.request.response_mode = 'query' - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_query) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - self.request.response_mode = 'fragment' - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - @mock.patch('oauthlib.common.generate_token') - def test_no_prompt_authorization(self, generate_token): - generate_token.return_value = 'abc' - scope, info = self.auth.validate_authorization_request(self.request) - self.request.prompt = 'none' - self.assertRaises(OIDCNoPrompt, - self.auth.validate_authorization_request, - self.request) - - # prompt == none requires id token hint - bearer = BearerToken(self.mock_validator) - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - self.request.response_mode = 'query' - self.request.id_token_hint = 'me@email.com' - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_query) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - # Test alernative response modes - self.request.response_mode = 'fragment' - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) - - # Ensure silent authentication and authorization is done - self.mock_validator.validate_silent_login.return_value = False - self.mock_validator.validate_silent_authorization.return_value = True - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=login_required', h['Location']) - - self.mock_validator.validate_silent_login.return_value = True - self.mock_validator.validate_silent_authorization.return_value = False - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=consent_required', h['Location']) - - # ID token hint must match logged in user - self.mock_validator.validate_silent_authorization.return_value = True - self.mock_validator.validate_user_match.return_value = False - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=login_required', h['Location']) - - def set_scopes(self, client_id, code, client, request): - request.scopes = self.request.scopes - request.state = self.request.state - request.user = 'bob' - return True - - def test_create_token_response(self): - self.request.response_type = None - self.mock_validator.validate_code.side_effect = self.set_scopes - - bearer = BearerToken(self.mock_validator) - - h, token, s = self.auth.create_token_response(self.request, bearer) - token = json.loads(token) - self.assertEqual(self.mock_validator.save_token.call_count, 1) - self.assertIn('access_token', token) - self.assertIn('refresh_token', token) - self.assertIn('expires_in', token) - self.assertIn('scope', token) - self.assertIn('id_token', token) - self.assertIn('openid', token['scope']) - - self.mock_validator.reset_mock() - - self.request.scopes = ('hello', 'world') - h, token, s = self.auth.create_token_response(self.request, bearer) - token = json.loads(token) - self.assertEqual(self.mock_validator.save_token.call_count, 1) - self.assertIn('access_token', token) - self.assertIn('refresh_token', token) - self.assertIn('expires_in', token) - self.assertIn('scope', token) - self.assertNotIn('id_token', token) - self.assertNotIn('openid', token['scope']) - - -class OpenIDImplicitTest(TestCase): - - def setUp(self): - self.request = Request('http://a.b/path') - self.request.scopes = ('hello', 'openid') - self.request.expires_in = 1800 - self.request.client_id = 'abcdef' - self.request.response_type = 'id_token token' - self.request.redirect_uri = 'https://a.b/cb' - self.request.nonce = 'zxc' - self.request.state = 'abc' - - self.mock_validator = mock.MagicMock() - self.mock_validator.get_id_token.side_effect = get_id_token_mock - self.auth = OpenIDConnectImplicit(request_validator=self.mock_validator) - - token = 'MOCKED_TOKEN' - self.url_query = 'https://a.b/cb?state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token - self.url_fragment = 'https://a.b/cb#state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token - - @mock.patch('oauthlib.common.generate_token') - def test_authorization(self, generate_token): - scope, info = self.auth.validate_authorization_request(self.request) - - generate_token.return_value = 'abc' - bearer = BearerToken(self.mock_validator) - - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - self.request.response_type = 'id_token' - token = 'MOCKED_TOKEN' - url = 'https://a.b/cb#state=abc&id_token=%s' % token - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], url, parse_fragment=True) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - self.request.nonce = None - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - @mock.patch('oauthlib.common.generate_token') - def test_no_prompt_authorization(self, generate_token): - generate_token.return_value = 'abc' - scope, info = self.auth.validate_authorization_request(self.request) - self.request.prompt = 'none' - self.assertRaises(OIDCNoPrompt, - self.auth.validate_authorization_request, - self.request) - - # prompt == none requires id token hint - bearer = BearerToken(self.mock_validator) - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - self.request.id_token_hint = 'me@email.com' - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - # Test alernative response modes - self.request.response_mode = 'query' - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_query) - - # Ensure silent authentication and authorization is done - self.mock_validator.validate_silent_login.return_value = False - self.mock_validator.validate_silent_authorization.return_value = True - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=login_required', h['Location']) - - self.mock_validator.validate_silent_login.return_value = True - self.mock_validator.validate_silent_authorization.return_value = False - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=consent_required', h['Location']) - - # ID token hint must match logged in user - self.mock_validator.validate_silent_authorization.return_value = True - self.mock_validator.validate_user_match.return_value = False - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=login_required', h['Location']) - - -class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeTokenTest, self).setUp() - self.request.response_type = 'code token' - self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator) - self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' - self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' - - -class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeIdTokenTest, self).setUp() - self.request.response_type = 'code id_token' - self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator) - token = 'MOCKED_TOKEN' - self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token - self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token - - -class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() - self.request.response_type = 'code id_token token' - self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator) - token = 'MOCKED_TOKEN' - self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token - self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token - - -class ImplicitTokenGrantDispatcherTest(TestCase): - def setUp(self): - self.request = Request('http://a.b/path') - request_validator = mock.MagicMock() - implicit_grant = ImplicitGrant(request_validator) - openid_connect_implicit = OpenIDConnectImplicit(request_validator) - - self.dispatcher = ImplicitTokenGrantDispatcher( - default_implicit_grant=implicit_grant, - oidc_implicit_grant=openid_connect_implicit - ) - - def test_create_authorization_response_openid(self): - self.request.scopes = ('hello', 'openid') - self.request.response_type = 'id_token' - handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, OpenIDConnectImplicit)) - - def test_validate_authorization_request_openid(self): - self.request.scopes = ('hello', 'openid') - self.request.response_type = 'id_token' - handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, OpenIDConnectImplicit)) - - def test_create_authorization_response_oauth(self): - self.request.scopes = ('hello', 'world') - handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, ImplicitGrant)) - - def test_validate_authorization_request_oauth(self): - self.request.scopes = ('hello', 'world') - handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, ImplicitGrant)) - - -class DispatcherTest(TestCase): - def setUp(self): - self.request = Request('http://a.b/path') - self.request.decoded_body = ( - ("client_id", "me"), - ("code", "code"), - ("redirect_url", "https://a.b/cb"), - ) - - self.request_validator = mock.MagicMock() - self.auth_grant = AuthorizationCodeGrant(self.request_validator) - self.openid_connect_auth = OpenIDConnectAuthCode(self.request_validator) - - -class AuthTokenGrantDispatcherOpenIdTest(DispatcherTest): - - def setUp(self): - super(AuthTokenGrantDispatcherOpenIdTest, self).setUp() - self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid') - self.dispatcher = AuthTokenGrantDispatcher( - self.request_validator, - default_token_grant=self.auth_grant, - oidc_token_grant=self.openid_connect_auth - ) - - def test_create_token_response_openid(self): - handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, OpenIDConnectAuthCode)) - self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) - - -class AuthTokenGrantDispatcherOpenIdWithoutCodeTest(DispatcherTest): - - def setUp(self): - super(AuthTokenGrantDispatcherOpenIdWithoutCodeTest, self).setUp() - self.request.decoded_body = ( - ("client_id", "me"), - ("code", ""), - ("redirect_url", "https://a.b/cb"), - ) - self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid') - self.dispatcher = AuthTokenGrantDispatcher( - self.request_validator, - default_token_grant=self.auth_grant, - oidc_token_grant=self.openid_connect_auth - ) - - def test_create_token_response_openid_without_code(self): - handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, AuthorizationCodeGrant)) - self.assertFalse(self.dispatcher.request_validator.get_authorization_code_scopes.called) - - -class AuthTokenGrantDispatcherOAuthTest(DispatcherTest): - - def setUp(self): - super(AuthTokenGrantDispatcherOAuthTest, self).setUp() - self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'world') - self.dispatcher = AuthTokenGrantDispatcher( - self.request_validator, - default_token_grant=self.auth_grant, - oidc_token_grant=self.openid_connect_auth - ) - - def test_create_token_response_oauth(self): - handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, AuthorizationCodeGrant)) - self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) diff --git a/tests/oauth2/rfc6749/test_server.py b/tests/oauth2/rfc6749/test_server.py index da303ceb..bc7a2b7c 100644 --- a/tests/oauth2/rfc6749/test_server.py +++ b/tests/oauth2/rfc6749/test_server.py @@ -3,21 +3,17 @@ import json -import jwt import mock from oauthlib import common from oauthlib.oauth2.rfc6749 import errors, tokens from oauthlib.oauth2.rfc6749.endpoints import Server -from oauthlib.oauth2.rfc6749.endpoints.authorization import \ - AuthorizationEndpoint +from oauthlib.oauth2.rfc6749.endpoints.authorization import AuthorizationEndpoint from oauthlib.oauth2.rfc6749.endpoints.resource import ResourceEndpoint from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint from oauthlib.oauth2.rfc6749.grant_types import (AuthorizationCodeGrant, ClientCredentialsGrant, ImplicitGrant, - OpenIDConnectAuthCode, - OpenIDConnectImplicit, ResourceOwnerPasswordCredentialsGrant) from ...unittest import TestCase @@ -29,40 +25,34 @@ def setUp(self): self.mock_validator = mock.MagicMock() self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) auth_code = AuthorizationCodeGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) auth_code.save_authorization_code = mock.MagicMock() implicit = ImplicitGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) implicit.save_token = mock.MagicMock() - openid_connect_auth = OpenIDConnectAuthCode(self.mock_validator) - openid_connect_implicit = OpenIDConnectImplicit(self.mock_validator) - response_types = { - 'code': auth_code, - 'token': implicit, - - 'id_token': openid_connect_implicit, - 'id_token token': openid_connect_implicit, - 'code token': openid_connect_auth, - 'code id_token': openid_connect_auth, - 'code token id_token': openid_connect_auth, - 'none': auth_code + 'code': auth_code, + 'token': implicit, + 'none': auth_code } self.expires_in = 1800 - token = tokens.BearerToken(self.mock_validator, - expires_in=self.expires_in) + token = tokens.BearerToken( + self.mock_validator, + expires_in=self.expires_in + ) self.endpoint = AuthorizationEndpoint( - default_response_type='code', - default_token_type=token, - response_types=response_types) + default_response_type='code', + default_token_type=token, + response_types=response_types + ) @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): uri = 'http://i.b/l?response_type=code&client_id=me&scope=all+of+them&state=xyz' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me?code=abc&state=xyz') @@ -71,7 +61,7 @@ def test_implicit_grant(self): uri = 'http://i.b/l?response_type=token&client_id=me&scope=all+of+them&state=xyz' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me#access_token=abc&expires_in=' + str(self.expires_in) + '&token_type=Bearer&state=xyz&scope=all+of+them', parse_fragment=True) @@ -79,7 +69,7 @@ def test_none_grant(self): uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them&state=xyz' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me?state=xyz', parse_fragment=True) self.assertEqual(body, None) @@ -99,9 +89,9 @@ def test_missing_type(self): uri = 'http://i.b/l?client_id=me&scope=all+of+them' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' self.mock_validator.validate_request = mock.MagicMock( - side_effect=errors.InvalidRequestError()) + side_effect=errors.InvalidRequestError()) headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me?error=invalid_request&error_description=Missing+response_type+parameter.') @@ -109,9 +99,9 @@ def test_invalid_type(self): uri = 'http://i.b/l?response_type=invalid&client_id=me&scope=all+of+them' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' self.mock_validator.validate_request = mock.MagicMock( - side_effect=errors.UnsupportedResponseTypeError()) + side_effect=errors.UnsupportedResponseTypeError()) headers, body, status_code = self.endpoint.create_authorization_response( - uri, scopes=['all', 'of', 'them']) + uri, scopes=['all', 'of', 'them']) self.assertIn('Location', headers) self.assertURLEqual(headers['Location'], 'http://back.to/me?error=unsupported_response_type') @@ -129,27 +119,32 @@ def set_user(request): self.mock_validator.authenticate_client.side_effect = set_user self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) auth_code = AuthorizationCodeGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) password = ResourceOwnerPasswordCredentialsGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) client = ClientCredentialsGrant( - request_validator=self.mock_validator) + request_validator=self.mock_validator) supported_types = { - 'authorization_code': auth_code, - 'password': password, - 'client_credentials': client, + 'authorization_code': auth_code, + 'password': password, + 'client_credentials': client, } self.expires_in = 1800 - token = tokens.BearerToken(self.mock_validator, - expires_in=self.expires_in) - self.endpoint = TokenEndpoint('authorization_code', - default_token_type=token, grant_types=supported_types) + token = tokens.BearerToken( + self.mock_validator, + expires_in=self.expires_in + ) + self.endpoint = TokenEndpoint( + 'authorization_code', + default_token_type=token, + grant_types=supported_types + ) @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): body = 'grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) token = { 'token_type': 'Bearer', 'expires_in': self.expires_in, @@ -176,7 +171,7 @@ def test_authorization_grant(self): def test_password_grant(self): body = 'grant_type=password&username=a&password=hello&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) token = { 'token_type': 'Bearer', 'expires_in': self.expires_in, @@ -190,7 +185,7 @@ def test_password_grant(self): def test_client_grant(self): body = 'grant_type=client_credentials&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) token = { 'token_type': 'Bearer', 'expires_in': self.expires_in, @@ -281,7 +276,7 @@ def set_user(request): def test_authorization_grant(self): body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) body = json.loads(body) token = { 'token_type': 'Bearer', @@ -295,7 +290,7 @@ def test_authorization_grant(self): body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&state=xyz' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) body = json.loads(body) token = { 'token_type': 'Bearer', @@ -310,7 +305,7 @@ def test_authorization_grant(self): def test_password_grant(self): body = 'grant_type=password&username=a&password=hello&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) body = json.loads(body) token = { 'token_type': 'Bearer', @@ -325,7 +320,7 @@ def test_password_grant(self): def test_scopes_and_user_id_stored_in_access_token(self): body = 'grant_type=password&username=a&password=hello&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) access_token = json.loads(body)['access_token'] @@ -338,7 +333,7 @@ def test_scopes_and_user_id_stored_in_access_token(self): def test_client_grant(self): body = 'grant_type=client_credentials&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( - '', body=body) + '', body=body) body = json.loads(body) token = { 'token_type': 'Bearer', @@ -366,8 +361,10 @@ def setUp(self): self.mock_validator = mock.MagicMock() self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) token = tokens.BearerToken(request_validator=self.mock_validator) - self.endpoint = ResourceEndpoint(default_token='Bearer', - token_types={'Bearer': token}) + self.endpoint = ResourceEndpoint( + default_token='Bearer', + token_types={'Bearer': token} + ) def test_defaults(self): uri = 'http://a.b/path?some=query' diff --git a/tests/oauth2/rfc6749/test_tokens.py b/tests/oauth2/rfc6749/test_tokens.py index ecac03e2..061754f6 100644 --- a/tests/oauth2/rfc6749/test_tokens.py +++ b/tests/oauth2/rfc6749/test_tokens.py @@ -1,9 +1,11 @@ from __future__ import absolute_import, unicode_literals -import mock - -from oauthlib.common import Request -from oauthlib.oauth2.rfc6749.tokens import * +from oauthlib.oauth2.rfc6749.tokens import ( + prepare_mac_header, + prepare_bearer_headers, + prepare_bearer_body, + prepare_bearer_uri, +) from ...unittest import TestCase @@ -96,196 +98,3 @@ def test_prepare_bearer_request(self): self.assertEqual(prepare_bearer_headers(self.token), self.bearer_headers) self.assertEqual(prepare_bearer_body(self.token), self.bearer_body) self.assertEqual(prepare_bearer_uri(self.token, uri=self.uri), self.bearer_uri) - - def test_fake_bearer_is_not_validated(self): - request_validator = mock.MagicMock() - request_validator.validate_bearer_token = self._mocked_validate_bearer_token - - for fake_header in self.fake_bearer_headers: - request = Request('/', headers=fake_header) - result = BearerToken(request_validator=request_validator).validate_request(request) - - self.assertFalse(result) - - def test_header_with_multispaces_is_validated(self): - request_validator = mock.MagicMock() - request_validator.validate_bearer_token = self._mocked_validate_bearer_token - - request = Request('/', headers=self.valid_header_with_multiple_spaces) - result = BearerToken(request_validator=request_validator).validate_request(request) - - self.assertTrue(result) - - def test_estimate_type_with_fake_header_returns_type_0(self): - request_validator = mock.MagicMock() - request_validator.validate_bearer_token = self._mocked_validate_bearer_token - - for fake_header in self.fake_bearer_headers: - request = Request('/', headers=fake_header) - result = BearerToken(request_validator=request_validator).estimate_type(request) - - if fake_header['Authorization'].count(' ') == 2 and \ - fake_header['Authorization'].split()[0] == 'Bearer': - # If we're dealing with the header containing 2 spaces, it will be recognized - # as a Bearer valid header, the token itself will be invalid by the way. - self.assertEqual(result, 9) - else: - self.assertEqual(result, 0) - - -class JWTTokenTestCase(TestCase): - fake_bearer_headers = [ - {'Authorization': 'Beaver vF9dft4qmT'}, - {'Authorization': 'BeavervF9dft4qmT'}, - {'Authorization': 'Beaver vF9dft4qmT'}, - {'Authorization': 'BearerF9dft4qmT'}, - {'Authorization': 'Bearer vF9df t4qmT'}, - ] - - valid_header_with_multiple_spaces = {'Authorization': 'Bearer vF9dft4qmT'} - - def _mocked_validate_bearer_token(self, token, scopes, request): - if not token: - return False - return True - - def test_create_token_callable_expires_in(self): - """ - Test retrieval of the expires in value by calling the callable expires_in property - """ - - expires_in_mock = mock.MagicMock() - request_mock = mock.MagicMock() - - token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock()) - token.create_token(request=request_mock) - - expires_in_mock.assert_called_once_with(request_mock) - - def test_create_token_non_callable_expires_in(self): - """ - When a non callable expires in is set this should just be set to the request - """ - - expires_in_mock = mock.NonCallableMagicMock() - request_mock = mock.MagicMock() - - token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock()) - token.create_token(request=request_mock) - - self.assertFalse(expires_in_mock.called) - self.assertEqual(request_mock.expires_in, expires_in_mock) - - def test_create_token_calls_get_id_token(self): - """ - When create_token is called the call should be forwarded to the get_id_token on the token validator - """ - request_mock = mock.MagicMock() - - with mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', - autospec=True) as RequestValidatorMock: - - request_validator = RequestValidatorMock() - - token = JWTToken(expires_in=mock.MagicMock(), request_validator=request_validator) - token.create_token(request=request_mock) - - request_validator.get_jwt_bearer_token.assert_called_once_with(None, None, request_mock) - - def test_validate_request_token_from_headers(self): - """ - Bearer token get retrieved from headers. - """ - - with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \ - mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', - autospec=True) as RequestValidatorMock: - request_validator_mock = RequestValidatorMock() - - token = JWTToken(request_validator=request_validator_mock) - - request = RequestMock('/uri') - # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch - # with autospec=True - request.scopes = mock.MagicMock() - request.headers = { - 'Authorization': 'Bearer some-token-from-header' - } - - token.validate_request(request=request) - - request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-header', - request.scopes, - request) - - def test_validate_token_from_request(self): - """ - Token get retrieved from request object. - """ - - with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \ - mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', - autospec=True) as RequestValidatorMock: - request_validator_mock = RequestValidatorMock() - - token = JWTToken(request_validator=request_validator_mock) - - request = RequestMock('/uri') - # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch - # with autospec=True - request.scopes = mock.MagicMock() - request.access_token = 'some-token-from-request-object' - request.headers = {} - - token.validate_request(request=request) - - request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-request-object', - request.scopes, - request) - - def test_fake_bearer_is_not_validated(self): - request_validator = mock.MagicMock() - request_validator.validate_jwt_bearer_token = self._mocked_validate_bearer_token - - for fake_header in self.fake_bearer_headers: - request = Request('/', headers=fake_header) - result = JWTToken(request_validator=request_validator).validate_request(request) - - self.assertFalse(result) - - def test_header_with_multiple_spaces_is_validated(self): - request_validator = mock.MagicMock() - request_validator.validate_jwt_bearer_token = self._mocked_validate_bearer_token - request = Request('/', headers=self.valid_header_with_multiple_spaces) - result = JWTToken(request_validator=request_validator).validate_request(request) - - self.assertTrue(result) - - def test_estimate_type(self): - """ - Estimate type results for a jwt token - """ - - def test_token(token, expected_result): - with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock: - jwt_token = JWTToken() - - request = RequestMock('/uri') - # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch - # with autospec=True - request.headers = { - 'Authorization': 'Bearer {}'.format(token) - } - - result = jwt_token.estimate_type(request=request) - - self.assertEqual(result, expected_result) - - test_items = ( - ('eyfoo.foo.foo', 10), - ('eyfoo.foo.foo.foo.foo', 10), - ('eyfoobar', 0) - ) - - for token, expected_result in test_items: - test_token(token, expected_result) diff --git a/tests/openid/__init__.py b/tests/openid/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/openid/connect/__init__.py b/tests/openid/connect/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/openid/connect/core/__init__.py b/tests/openid/connect/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/oauth2/rfc6749/endpoints/test_claims_handling.py b/tests/openid/connect/core/endpoints/test_claims_handling.py similarity index 86% rename from tests/oauth2/rfc6749/endpoints/test_claims_handling.py rename to tests/openid/connect/core/endpoints/test_claims_handling.py index ff72673f..37a7cdda 100644 --- a/tests/oauth2/rfc6749/endpoints/test_claims_handling.py +++ b/tests/openid/connect/core/endpoints/test_claims_handling.py @@ -10,10 +10,12 @@ import mock -from oauthlib.oauth2 import InvalidRequestError, RequestValidator, Server +from oauthlib.oauth2 import RequestValidator + +from oauthlib.oauth2.rfc6749.endpoints.pre_configured import Server from ....unittest import TestCase -from .test_utils import get_fragment_credentials, get_query_credentials +from .test_utils import get_query_credentials class TestClaimsHandling(TestCase): @@ -81,7 +83,7 @@ def test_claims_stored_on_code_creation(self): } } - claims_urlquoted='%7B%22id_token%22%3A%20%7B%22claim_2%22%3A%20%7B%22essential%22%3A%20true%7D%2C%20%22claim_1%22%3A%20null%7D%2C%20%22userinfo%22%3A%20%7B%22claim_4%22%3A%20null%2C%20%22claim_3%22%3A%20%7B%22essential%22%3A%20true%7D%7D%7D' + claims_urlquoted = '%7B%22id_token%22%3A%20%7B%22claim_2%22%3A%20%7B%22essential%22%3A%20true%7D%2C%20%22claim_1%22%3A%20null%7D%2C%20%22userinfo%22%3A%20%7B%22claim_4%22%3A%20null%2C%20%22claim_3%22%3A%20%7B%22essential%22%3A%20true%7D%7D%7D' uri = 'http://example.com/path?client_id=abc&scope=openid+test_scope&response_type=code&claims=%s' h, b, s = self.server.create_authorization_response(uri % claims_urlquoted, scopes='openid test_scope') @@ -90,8 +92,10 @@ def test_claims_stored_on_code_creation(self): code = get_query_credentials(h['Location'])['code'][0] token_uri = 'http://example.com/path' - _, body, _ = self.server.create_token_response(token_uri, - body='client_id=me&redirect_uri=http://back.to/me&grant_type=authorization_code&code=%s' % code) + _, body, _ = self.server.create_token_response( + token_uri, + body='client_id=me&redirect_uri=http://back.to/me&grant_type=authorization_code&code=%s' % code + ) self.assertDictEqual(self.claims_saved_with_bearer_token, claims) diff --git a/tests/oauth2/rfc6749/endpoints/test_openid_connect_params_handling.py b/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py similarity index 100% rename from tests/oauth2/rfc6749/endpoints/test_openid_connect_params_handling.py rename to tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py diff --git a/tests/openid/connect/core/grant_types/test_authorization_code.py b/tests/openid/connect/core/grant_types/test_authorization_code.py new file mode 100644 index 00000000..1bad120f --- /dev/null +++ b/tests/openid/connect/core/grant_types/test_authorization_code.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import json + +import mock + +from oauthlib.common import Request +from oauthlib.oauth2.rfc6749.tokens import BearerToken + +from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant +from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt + +from ....unittest import TestCase +from ....oauth2.rfc6749.grant_types.test_authorization_code import AuthorizationCodeGrantTest + + +def get_id_token_mock(token, token_handler, request): + return "MOCKED_TOKEN" + + +class OpenIDAuthCodeInterferenceTest(AuthorizationCodeGrantTest): + """Test that OpenID don't interfere with normal OAuth 2 flows.""" + + def setUp(self): + super(OpenIDAuthCodeInterferenceTest, self).setUp() + self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator) + + +class OpenIDAuthCodeTest(TestCase): + + def setUp(self): + self.request = Request('http://a.b/path') + self.request.scopes = ('hello', 'openid') + self.request.expires_in = 1800 + self.request.client_id = 'abcdef' + self.request.code = '1234' + self.request.response_type = 'code' + self.request.grant_type = 'authorization_code' + self.request.redirect_uri = 'https://a.b/cb' + self.request.state = 'abc' + + self.mock_validator = mock.MagicMock() + self.mock_validator.authenticate_client.side_effect = self.set_client + self.mock_validator.get_id_token.side_effect = get_id_token_mock + self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator) + + self.url_query = 'https://a.b/cb?code=abc&state=abc' + self.url_fragment = 'https://a.b/cb#code=abc&state=abc' + + def set_client(self, request): + request.client = mock.MagicMock() + request.client.client_id = 'mocked' + return True + + @mock.patch('oauthlib.common.generate_token') + def test_authorization(self, generate_token): + + scope, info = self.auth.validate_authorization_request(self.request) + + generate_token.return_value = 'abc' + bearer = BearerToken(self.mock_validator) + self.request.response_mode = 'query' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_query) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + self.request.response_mode = 'fragment' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + @mock.patch('oauthlib.common.generate_token') + def test_no_prompt_authorization(self, generate_token): + generate_token.return_value = 'abc' + scope, info = self.auth.validate_authorization_request(self.request) + self.request.prompt = 'none' + self.assertRaises(OIDCNoPrompt, + self.auth.validate_authorization_request, + self.request) + + # prompt == none requires id token hint + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + self.request.response_mode = 'query' + self.request.id_token_hint = 'me@email.com' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_query) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + # Test alernative response modes + self.request.response_mode = 'fragment' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) + + # Ensure silent authentication and authorization is done + self.mock_validator.validate_silent_login.return_value = False + self.mock_validator.validate_silent_authorization.return_value = True + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=login_required', h['Location']) + + self.mock_validator.validate_silent_login.return_value = True + self.mock_validator.validate_silent_authorization.return_value = False + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=consent_required', h['Location']) + + # ID token hint must match logged in user + self.mock_validator.validate_silent_authorization.return_value = True + self.mock_validator.validate_user_match.return_value = False + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=login_required', h['Location']) + + def set_scopes(self, client_id, code, client, request): + request.scopes = self.request.scopes + request.state = self.request.state + request.user = 'bob' + return True + + def test_create_token_response(self): + self.request.response_type = None + self.mock_validator.validate_code.side_effect = self.set_scopes + + bearer = BearerToken(self.mock_validator) + + h, token, s = self.auth.create_token_response(self.request, bearer) + token = json.loads(token) + self.assertEqual(self.mock_validator.save_token.call_count, 1) + self.assertIn('access_token', token) + self.assertIn('refresh_token', token) + self.assertIn('expires_in', token) + self.assertIn('scope', token) + self.assertIn('id_token', token) + self.assertIn('openid', token['scope']) + + self.mock_validator.reset_mock() + + self.request.scopes = ('hello', 'world') + h, token, s = self.auth.create_token_response(self.request, bearer) + token = json.loads(token) + self.assertEqual(self.mock_validator.save_token.call_count, 1) + self.assertIn('access_token', token) + self.assertIn('refresh_token', token) + self.assertIn('expires_in', token) + self.assertIn('scope', token) + self.assertNotIn('id_token', token) + self.assertNotIn('openid', token['scope']) diff --git a/tests/openid/connect/core/grant_types/test_dispatchers.py b/tests/openid/connect/core/grant_types/test_dispatchers.py new file mode 100644 index 00000000..f90ec468 --- /dev/null +++ b/tests/openid/connect/core/grant_types/test_dispatchers.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals +import mock + +from oauthlib.common import Request + +from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant +from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant +from oauthlib.openid.connect.core.grant_types.dispatchers import ( + ImplicitTokenGrantDispatcher, + AuthorizationTokenGrantDispatcher +) + +from oauthlib.oauth2.rfc6749.grant_types import ( + AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant, + ImplicitGrant as OAuth2ImplicitGrant, +) + + +from ....unittest import TestCase + + +class ImplicitTokenGrantDispatcherTest(TestCase): + def setUp(self): + self.request = Request('http://a.b/path') + request_validator = mock.MagicMock() + implicit_grant = OAuth2ImplicitGrant(request_validator) + openid_connect_implicit = ImplicitGrant(request_validator) + + self.dispatcher = ImplicitTokenGrantDispatcher( + default_implicit_grant=implicit_grant, + oidc_implicit_grant=openid_connect_implicit + ) + + def test_create_authorization_response_openid(self): + self.request.scopes = ('hello', 'openid') + self.request.response_type = 'id_token' + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, ImplicitGrant)) + + def test_validate_authorization_request_openid(self): + self.request.scopes = ('hello', 'openid') + self.request.response_type = 'id_token' + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, ImplicitGrant)) + + def test_create_authorization_response_oauth(self): + self.request.scopes = ('hello', 'world') + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, ImplicitGrant)) + + def test_validate_authorization_request_oauth(self): + self.request.scopes = ('hello', 'world') + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, ImplicitGrant)) + + +class DispatcherTest(TestCase): + def setUp(self): + self.request = Request('http://a.b/path') + self.request.decoded_body = ( + ("client_id", "me"), + ("code", "code"), + ("redirect_url", "https://a.b/cb"), + ) + + self.request_validator = mock.MagicMock() + self.auth_grant = OAuth2AuthorizationCodeGrant(self.request_validator) + self.openid_connect_auth = OAuth2AuthorizationCodeGrant(self.request_validator) + + +class AuthTokenGrantDispatcherOpenIdTest(DispatcherTest): + + def setUp(self): + super(AuthTokenGrantDispatcherOpenIdTest, self).setUp() + self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid') + self.dispatcher = AuthorizationTokenGrantDispatcher( + self.request_validator, + default_token_grant=self.auth_grant, + oidc_token_grant=self.openid_connect_auth + ) + + def test_create_token_response_openid(self): + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, AuthorizationCodeGrant)) + self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) + + +class AuthTokenGrantDispatcherOpenIdWithoutCodeTest(DispatcherTest): + + def setUp(self): + super(AuthTokenGrantDispatcherOpenIdWithoutCodeTest, self).setUp() + self.request.decoded_body = ( + ("client_id", "me"), + ("code", ""), + ("redirect_url", "https://a.b/cb"), + ) + self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid') + self.dispatcher = AuthorizationTokenGrantDispatcher( + self.request_validator, + default_token_grant=self.auth_grant, + oidc_token_grant=self.openid_connect_auth + ) + + def test_create_token_response_openid_without_code(self): + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, OAuth2AuthorizationCodeGrant)) + self.assertFalse(self.dispatcher.request_validator.get_authorization_code_scopes.called) + + +class AuthTokenGrantDispatcherOAuthTest(DispatcherTest): + + def setUp(self): + super(AuthTokenGrantDispatcherOAuthTest, self).setUp() + self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'world') + self.dispatcher = AuthorizationTokenGrantDispatcher( + self.request_validator, + default_token_grant=self.auth_grant, + oidc_token_grant=self.openid_connect_auth + ) + + def test_create_token_response_oauth(self): + handler = self.dispatcher._handler_for_request(self.request) + self.assertTrue(isinstance(handler, OAuth2AuthorizationCodeGrant)) + self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) diff --git a/tests/openid/connect/core/grant_types/test_hybrid.py b/tests/openid/connect/core/grant_types/test_hybrid.py new file mode 100644 index 00000000..531ae7f2 --- /dev/null +++ b/tests/openid/connect/core/grant_types/test_hybrid.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant + +from ....oauth2.rfc6749.grant_types.test_authorization_code import AuthorizationCodeGrantTest + + +class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): + """Test that OpenID don't interfere with normal OAuth 2 flows.""" + + def setUp(self): + super(OpenIDHybridInterferenceTest, self).setUp() + self.auth = HybridGrant(request_validator=self.mock_validator) diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py new file mode 100644 index 00000000..56247d99 --- /dev/null +++ b/tests/openid/connect/core/grant_types/test_implicit.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import mock + +from oauthlib.common import Request + +from oauthlib.oauth2.rfc6749.tokens import BearerToken + +from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant +from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt + +from ....unittest import TestCase +from .test_authorization_code import get_id_token_mock, OpenIDAuthCodeTest + +from ....oauth2.rfc6749.grant_types.test_implicit import ImplicitGrantTest + + +class OpenIDImplicitInterferenceTest(ImplicitGrantTest): + """Test that OpenID don't interfere with normal OAuth 2 flows.""" + + def setUp(self): + super(OpenIDImplicitInterferenceTest, self).setUp() + self.auth = ImplicitGrant(request_validator=self.mock_validator) + + +class OpenIDImplicitTest(TestCase): + + def setUp(self): + self.request = Request('http://a.b/path') + self.request.scopes = ('hello', 'openid') + self.request.expires_in = 1800 + self.request.client_id = 'abcdef' + self.request.response_type = 'id_token token' + self.request.redirect_uri = 'https://a.b/cb' + self.request.nonce = 'zxc' + self.request.state = 'abc' + + self.mock_validator = mock.MagicMock() + self.mock_validator.get_id_token.side_effect = get_id_token_mock + self.auth = ImplicitGrant(request_validator=self.mock_validator) + + token = 'MOCKED_TOKEN' + self.url_query = 'https://a.b/cb?state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + self.url_fragment = 'https://a.b/cb#state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + + @mock.patch('oauthlib.common.generate_token') + def test_authorization(self, generate_token): + scope, info = self.auth.validate_authorization_request(self.request) + + generate_token.return_value = 'abc' + bearer = BearerToken(self.mock_validator) + + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + self.request.response_type = 'id_token' + token = 'MOCKED_TOKEN' + url = 'https://a.b/cb#state=abc&id_token=%s' % token + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], url, parse_fragment=True) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + self.request.nonce = None + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + @mock.patch('oauthlib.common.generate_token') + def test_no_prompt_authorization(self, generate_token): + generate_token.return_value = 'abc' + scope, info = self.auth.validate_authorization_request(self.request) + self.request.prompt = 'none' + self.assertRaises(OIDCNoPrompt, + self.auth.validate_authorization_request, + self.request) + + # prompt == none requires id token hint + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + self.request.id_token_hint = 'me@email.com' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + # Test alernative response modes + self.request.response_mode = 'query' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_query) + + # Ensure silent authentication and authorization is done + self.mock_validator.validate_silent_login.return_value = False + self.mock_validator.validate_silent_authorization.return_value = True + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=login_required', h['Location']) + + self.mock_validator.validate_silent_login.return_value = True + self.mock_validator.validate_silent_authorization.return_value = False + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=consent_required', h['Location']) + + # ID token hint must match logged in user + self.mock_validator.validate_silent_authorization.return_value = True + self.mock_validator.validate_user_match.return_value = False + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=login_required', h['Location']) + + +class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeTokenTest, self).setUp() + self.request.response_type = 'code token' + self.auth = HybridGrant(request_validator=self.mock_validator) + self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' + self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' + + +class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeIdTokenTest, self).setUp() + self.request.response_type = 'code id_token' + self.auth = HybridGrant(request_validator=self.mock_validator) + token = 'MOCKED_TOKEN' + self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token + self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token + + +class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() + self.request.response_type = 'code id_token token' + self.auth = HybridGrant(request_validator=self.mock_validator) + token = 'MOCKED_TOKEN' + self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token diff --git a/tests/openid/connect/core/test_request_validator.py b/tests/openid/connect/core/test_request_validator.py new file mode 100644 index 00000000..14a7c232 --- /dev/null +++ b/tests/openid/connect/core/test_request_validator.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +from oauthlib.openid.connect.core.request_validator import RequestValidator + +from ....unittest import TestCase + + +class RequestValidatorTest(TestCase): + + def test_method_contracts(self): + v = RequestValidator() + self.assertRaises( + NotImplementedError, + v.get_authorization_code_scopes, + 'client_id', 'code', 'redirect_uri', 'request' + ) + self.assertRaises( + NotImplementedError, + v.get_jwt_bearer_token, + 'token', 'token_handler', 'request' + ) + self.assertRaises( + NotImplementedError, + v.get_id_token, + 'token', 'token_handler', 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_jwt_bearer_token, + 'token', 'scopes', 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_id_token, + 'token', 'scopes', 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_silent_authorization, + 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_silent_login, + 'request' + ) + self.assertRaises( + NotImplementedError, + v.validate_user_match, + 'id_token_hint', 'scopes', 'claims', 'request' + ) diff --git a/tests/openid/connect/core/test_server.py b/tests/openid/connect/core/test_server.py new file mode 100644 index 00000000..83290db8 --- /dev/null +++ b/tests/openid/connect/core/test_server.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import json + +import mock + +from oauthlib.oauth2.rfc6749 import errors +from oauthlib.oauth2.rfc6749.endpoints.authorization import AuthorizationEndpoint +from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint +from oauthlib.oauth2.rfc6749.tokens import BearerToken + +from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant +from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant + +from ....unittest import TestCase + + +class AuthorizationEndpointTest(TestCase): + + def setUp(self): + self.mock_validator = mock.MagicMock() + self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) + auth_code = AuthorizationCodeGrant(request_validator=self.mock_validator) + auth_code.save_authorization_code = mock.MagicMock() + implicit = ImplicitGrant( + request_validator=self.mock_validator) + implicit.save_token = mock.MagicMock() + hybrid = HybridGrant(self.mock_validator) + + response_types = { + 'code': auth_code, + 'token': implicit, + 'id_token': implicit, + 'id_token token': implicit, + 'code token': hybrid, + 'code id_token': hybrid, + 'code token id_token': hybrid, + 'none': auth_code + } + self.expires_in = 1800 + token = BearerToken( + self.mock_validator, + expires_in=self.expires_in + ) + self.endpoint = AuthorizationEndpoint( + default_response_type='code', + default_token_type=token, + response_types=response_types + ) + + # TODO: Add hybrid grant test + + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') + def test_authorization_grant(self): + uri = 'http://i.b/l?response_type=code&client_id=me&scope=all+of+them&state=xyz' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me?code=abc&state=xyz') + + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') + def test_implicit_grant(self): + uri = 'http://i.b/l?response_type=token&client_id=me&scope=all+of+them&state=xyz' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me#access_token=abc&expires_in=' + str(self.expires_in) + '&token_type=Bearer&state=xyz&scope=all+of+them', parse_fragment=True) + + def test_none_grant(self): + uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them&state=xyz' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me?state=xyz', parse_fragment=True) + self.assertEqual(body, None) + self.assertEqual(status_code, 302) + + # and without the state parameter + uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me', parse_fragment=True) + self.assertEqual(body, None) + self.assertEqual(status_code, 302) + + def test_missing_type(self): + uri = 'http://i.b/l?client_id=me&scope=all+of+them' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + self.mock_validator.validate_request = mock.MagicMock( + side_effect=errors.InvalidRequestError()) + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me?error=invalid_request&error_description=Missing+response_type+parameter.') + + def test_invalid_type(self): + uri = 'http://i.b/l?response_type=invalid&client_id=me&scope=all+of+them' + uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' + self.mock_validator.validate_request = mock.MagicMock( + side_effect=errors.UnsupportedResponseTypeError()) + headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) + self.assertIn('Location', headers) + self.assertURLEqual(headers['Location'], 'http://back.to/me?error=unsupported_response_type') + + +class TokenEndpointTest(TestCase): + + def setUp(self): + def set_user(request): + request.user = mock.MagicMock() + request.client = mock.MagicMock() + request.client.client_id = 'mocked_client_id' + return True + + self.mock_validator = mock.MagicMock() + self.mock_validator.authenticate_client.side_effect = set_user + self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) + auth_code = AuthorizationCodeGrant( + request_validator=self.mock_validator) + supported_types = { + 'authorization_code': auth_code, + } + self.expires_in = 1800 + token = BearerToken( + self.mock_validator, + expires_in=self.expires_in + ) + self.endpoint = TokenEndpoint( + 'authorization_code', + default_token_type=token, + grant_types=supported_types + ) + + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') + def test_authorization_grant(self): + body = 'grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + token = { + 'token_type': 'Bearer', + 'expires_in': self.expires_in, + 'access_token': 'abc', + 'refresh_token': 'abc', + 'scope': 'all of them', + 'state': 'xyz' + } + self.assertEqual(json.loads(body), token) + + body = 'grant_type=authorization_code&code=abc&state=xyz' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + token = { + 'token_type': 'Bearer', + 'expires_in': self.expires_in, + 'access_token': 'abc', + 'refresh_token': 'abc', + 'state': 'xyz' + } + self.assertEqual(json.loads(body), token) + + def test_missing_type(self): + _, body, _ = self.endpoint.create_token_response('', body='') + token = {'error': 'unsupported_grant_type'} + self.assertEqual(json.loads(body), token) + + def test_invalid_type(self): + body = 'grant_type=invalid' + _, body, _ = self.endpoint.create_token_response('', body=body) + token = {'error': 'unsupported_grant_type'} + self.assertEqual(json.loads(body), token) diff --git a/tests/openid/connect/core/test_tokens.py b/tests/openid/connect/core/test_tokens.py new file mode 100644 index 00000000..12c75f10 --- /dev/null +++ b/tests/openid/connect/core/test_tokens.py @@ -0,0 +1,133 @@ +from __future__ import absolute_import, unicode_literals + +import mock + +from oauthlib.openid.connect.core.tokens import JWTToken + +from ....unittest import TestCase + + +class JWTTokenTestCase(TestCase): + + def test_create_token_callable_expires_in(self): + """ + Test retrieval of the expires in value by calling the callable expires_in property + """ + + expires_in_mock = mock.MagicMock() + request_mock = mock.MagicMock() + + token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock()) + token.create_token(request=request_mock) + + expires_in_mock.assert_called_once_with(request_mock) + + def test_create_token_non_callable_expires_in(self): + """ + When a non callable expires in is set this should just be set to the request + """ + + expires_in_mock = mock.NonCallableMagicMock() + request_mock = mock.MagicMock() + + token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock()) + token.create_token(request=request_mock) + + self.assertFalse(expires_in_mock.called) + self.assertEqual(request_mock.expires_in, expires_in_mock) + + def test_create_token_calls_get_id_token(self): + """ + When create_token is called the call should be forwarded to the get_id_token on the token validator + """ + request_mock = mock.MagicMock() + + with mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + autospec=True) as RequestValidatorMock: + + request_validator = RequestValidatorMock() + + token = JWTToken(expires_in=mock.MagicMock(), request_validator=request_validator) + token.create_token(request=request_mock) + + request_validator.get_jwt_bearer_token.assert_called_once_with(None, None, request_mock) + + def test_validate_request_token_from_headers(self): + """ + Bearer token get retrieved from headers. + """ + + with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \ + mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + autospec=True) as RequestValidatorMock: + request_validator_mock = RequestValidatorMock() + + token = JWTToken(request_validator=request_validator_mock) + + request = RequestMock('/uri') + # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch + # with autospec=True + request.scopes = mock.MagicMock() + request.headers = { + 'Authorization': 'Bearer some-token-from-header' + } + + token.validate_request(request=request) + + request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-header', + request.scopes, + request) + + def test_validate_token_from_request(self): + """ + Token get retrieved from request object. + """ + + with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \ + mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + autospec=True) as RequestValidatorMock: + request_validator_mock = RequestValidatorMock() + + token = JWTToken(request_validator=request_validator_mock) + + request = RequestMock('/uri') + # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch + # with autospec=True + request.scopes = mock.MagicMock() + request.access_token = 'some-token-from-request-object' + request.headers = {} + + token.validate_request(request=request) + + request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-request-object', + request.scopes, + request) + + def test_estimate_type(self): + """ + Estimate type results for a jwt token + """ + + def test_token(token, expected_result): + with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock: + jwt_token = JWTToken() + + request = RequestMock('/uri') + # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch + # with autospec=True + request.headers = { + 'Authorization': 'Bearer {}'.format(token) + } + + result = jwt_token.estimate_type(request=request) + + self.assertEqual(result, expected_result) + + test_items = ( + ('eyfoo.foo.foo', 10), + ('eyfoo.foo.foo.foo.foo', 10), + ('eyfoobar', 0) + ) + + for token, expected_result in test_items: + test_token(token, expected_result) From 5b9b752f68d3a7963cb5b85cf5f9570490eacf7a Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 30 Jun 2018 14:55:59 -0700 Subject: [PATCH 048/237] Update all pypi.python.org URLs to pypi.org (#555) --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index b477e418..6741a758 100644 --- a/README.rst +++ b/README.rst @@ -11,10 +11,10 @@ logic for Python 2.7 and 3.4+.* :target: https://coveralls.io/r/oauthlib/oauthlib :alt: Coveralls .. image:: https://img.shields.io/pypi/pyversions/oauthlib.svg - :target: https://pypi.python.org/pypi/oauthlib + :target: https://pypi.org/project/oauthlib/ :alt: Download from PyPi .. image:: https://img.shields.io/pypi/l/oauthlib.svg - :target: https://pypi.python.org/pypi/oauthlib + :target: https://pypi.org/project/oauthlib/ :alt: License .. image:: https://img.shields.io/readthedocs/oauthlib.svg :target: https://oauthlib.readthedocs.io/en/latest/index.html From 481a4ec2e29530541ff8985cce938ece7a661562 Mon Sep 17 00:00:00 2001 From: claweyenuk <39317519+claweyenuk@users.noreply.github.com> Date: Sat, 30 Jun 2018 15:04:02 -0700 Subject: [PATCH 049/237] Update save_bearer_token docs to mention how the token is passed in as a reference (#556) --- oauthlib/oauth2/rfc6749/request_validator.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 92edba62..bf1515dc 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -332,7 +332,14 @@ def save_bearer_token(self, token, request, *args, **kwargs): } Note that while "scope" is a string-separated list of authorized scopes, - the original list is still available in request.scopes + the original list is still available in request.scopes. + + The token dict is passed as a reference so any changes made to the dictionary + will go back to the user. If additional information must return to the client + user, and it is only possible to get this information after writing the token + to storage, it should be added to the token dictionary. If the token + dictionary must be modified but the changes should not go back to the user, + a copy of the dictionary must be made before making the changes. Also note that if an Authorization Code grant request included a valid claims parameter (for OpenID Connect) then the request.claims property will contain From 3eaf962311dfbc566dbfa66a988e0331b91184be Mon Sep 17 00:00:00 2001 From: Seth Davis Date: Sat, 30 Jun 2018 18:09:26 -0400 Subject: [PATCH 050/237] Remove handling of nonstandard parameter "expires" (#506) --- oauthlib/oauth2/rfc6749/parameters.py | 7 ++----- tests/oauth2/rfc6749/test_parameters.py | 11 ----------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 01079338..9ea8c446 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -362,16 +362,13 @@ def parse_token_response(body, scope=None): # https://github.com/oauthlib/oauthlib/issues/267 params = dict(urlparse.parse_qsl(body)) - for key in ('expires_in', 'expires'): - if key in params: # cast a couple things to int + for key in ('expires_in',): + if key in params: # cast things to int params[key] = int(params[key]) if 'scope' in params: params['scope'] = scope_to_list(params['scope']) - if 'expires' in params: - params['expires_in'] = params.pop('expires') - if 'expires_in' in params: params['expires_at'] = time.time() + int(params['expires_in']) diff --git a/tests/oauth2/rfc6749/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py index 2a9cbe80..6ba98c02 100644 --- a/tests/oauth2/rfc6749/test_parameters.py +++ b/tests/oauth2/rfc6749/test_parameters.py @@ -115,13 +115,6 @@ def setUp(self): ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",' ' "example_parameter": "example_value" }') - json_expires = ('{ "access_token": "2YotnFZFEjr1zCsicMWpAA",' - ' "token_type": "example",' - ' "expires": 3600,' - ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",' - ' "example_parameter": "example_value",' - ' "scope":"abc def"}') - json_dict = { 'access_token': '2YotnFZFEjr1zCsicMWpAA', 'token_type': 'example', @@ -264,7 +257,3 @@ def record_scope_change(sender, message, old, new): finally: signals.scope_changed.disconnect(record_scope_change) del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] - - def test_token_response_with_expires(self): - """Verify fallback for alternate spelling of expires_in. """ - self.assertEqual(parse_token_response(self.json_expires), self.json_dict) From cfcbe99477a5d392175970f9c2e16b7d8ce138fb Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Mon, 2 Jul 2018 10:18:55 +0100 Subject: [PATCH 051/237] The id_token_hint parameter isn't required by the OIDC spec. (#559) --- oauthlib/openid/connect/core/grant_types/base.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index 2bb48b1e..fa578a57 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -225,12 +225,6 @@ def openid_authorization_validator(self, request): msg = "Prompt none is mutually exclusive with other values." raise InvalidRequestError(request=request, description=msg) - # prompt other than 'none' should be handled by the server code that - # uses oauthlib - if not request.id_token_hint: - msg = "Prompt is set to none yet id_token_hint is missing." - raise InvalidRequestError(request=request, description=msg) - if not self.request_validator.validate_silent_login(request): raise LoginRequired(request=request) From a4f39fc93ca2cb3b14eb1f3538ba5363148485be Mon Sep 17 00:00:00 2001 From: Free Duerinckx Date: Wed, 4 Jul 2018 14:35:03 +0200 Subject: [PATCH 052/237] `invalid_grant` status code should be 400 According to section 5.2 of rfc 6749 (https://tools.ietf.org/html/rfc6749#section-5.2) A server should respond with 400 in case of an invalid grant. The given grant is invalid and the client should give other data. A 401 is not applicable here because the client is required to give a suitable Authorization header field which doesn't make any sense if you are trying to acquire a grant authentication. According to sections 10.4.1 and 10.4.2 of rfc 2616 (https://tools.ietf.org/html/rfc2616#section-10.4.1) --- oauthlib/oauth2/rfc6749/errors.py | 2 +- tests/oauth2/rfc6749/grant_types/test_refresh_token.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 5a0cca2b..7b31d478 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -245,7 +245,7 @@ class InvalidGrantError(OAuth2Error): issued to another client. """ error = 'invalid_grant' - status_code = 401 + status_code = 400 class UnauthorizedClientError(OAuth2Error): diff --git a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py index 21540a21..f055c7d7 100644 --- a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py +++ b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py @@ -109,7 +109,7 @@ def test_invalid_token(self): token = json.loads(body) self.assertEqual(self.mock_validator.save_token.call_count, 0) self.assertEqual(token['error'], 'invalid_grant') - self.assertEqual(status_code, 401) + self.assertEqual(status_code, 400) def test_invalid_client(self): self.mock_validator.authenticate_client.return_value = False From f991b5759a4728fd99d36f98e2dd171b300fb7c2 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 17 Jul 2018 15:21:58 +0200 Subject: [PATCH 053/237] Added flask-dance tests, see #553 --- Makefile | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f9cc4abf..64fdc8e9 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ clean: clean-eggs clean-build @find . -iname '__pycache__' -delete rm -rf .tox rm -rf bottle-oauthlib + rm -rf dance rm -rf django-oauth-toolkit rm -rf flask-oauthlib rm -rf requests-oauthlib @@ -65,6 +66,13 @@ requests: cd requests-oauthlib 2>/dev/null || git clone https://github.com/requests/requests-oauthlib.git cd requests-oauthlib && sed -i.old 's,deps=,deps = --editable=file://{toxinidir}/../[signedtoken],' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox +dance: + #--------------------------- + # Library singingwolfboy/flask-dance + # Contacts: singingwolfboy + cd flask-dance 2>/dev/null || git clone https://github.com/singingwolfboy/flask-dance.git + cd flask-dance && sed -i.old 's,deps=,deps = --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox + .DEFAULT_GOAL := all -.PHONY: clean test bottle django flask requests -all: clean test bottle django flask requests +.PHONY: clean test bottle dance django flask requests +all: clean test bottle dance django flask requests From fbacd77b602e4c60f8da2413c150fa7f20b2f83c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 30 Jul 2018 13:43:00 +0200 Subject: [PATCH 054/237] Added htmlcov to help increase coverage locally --- .gitignore | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4515c8f6..683f357b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ pip-log.txt .coverage .tox coverage +htmlcov* #Translations *.mo diff --git a/tox.ini b/tox.ini index 3dded414..8f3345eb 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ envlist = py27,py34,py35,py36,pypy,docs [testenv] deps= -rrequirements-test.txt -commands=nosetests --with-coverage --cover-erase --cover-package=oauthlib -w tests +commands=nosetests --with-coverage --cover-html --cover-html-dir={toxinidir}/htmlcov-{envname} --cover-erase --cover-package=oauthlib -w tests [testenv:py27] deps=unittest2 From 3b6be54ab967d9ac6174fae97b5368c1d9f6c6c3 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 30 Jul 2018 14:48:24 +0200 Subject: [PATCH 055/237] Call get_default_redirect_uri if no redirect_uri in token req --- .../rfc6749/grant_types/authorization_code.py | 11 ++++++++++ .../test_credentials_preservation.py | 21 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 06602634..1ad67273 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -420,6 +420,17 @@ def validate_token_request(self, request): # REQUIRED, if the "redirect_uri" parameter was included in the # authorization request as described in Section 4.1.1, and their # values MUST be identical. + if request.redirect_uri is None: + request.using_default_redirect_uri = True + request.redirect_uri = self.request_validator.get_default_redirect_uri( + request.client_id, request) + log.debug('Using default redirect_uri %s.', request.redirect_uri) + if not request.redirect_uri: + raise errors.MissingRedirectURIError(request=request) + else: + request.using_default_redirect_uri = False + log.debug('Using provided redirect_uri %s', request.redirect_uri) + if not self.request_validator.confirm_redirect_uri(request.client_id, request.code, request.redirect_uri, request.client, request): diff --git a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py index 0eb719f4..50c2956d 100644 --- a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py +++ b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py @@ -116,3 +116,24 @@ def test_default_uri(self): self.assertRaises(errors.MissingRedirectURIError, self.mobile.create_authorization_response, auth_uri + '&response_type=token', scopes=['random']) + + def test_default_uri_in_token(self): + auth_uri = 'http://example.com/path?state=xyz&client_id=abc' + token_uri = 'http://example.com/path' + + # authorization grant + h, _, s = self.web.create_authorization_response( + auth_uri + '&response_type=code', scopes=['random']) + self.assertEqual(s, 302) + self.assertIn('Location', h) + self.assertTrue(h['Location'].startswith(self.DEFAULT_REDIRECT_URI)) + + # confirm_redirect_uri should return true if the redirect uri + # was not given in the authorization AND not in the token request. + self.validator.confirm_redirect_uri.return_value = True + code = get_query_credentials(h['Location'])['code'][0] + self.validator.validate_code.side_effect = self.set_state('xyz') + _, body, s = self.web.create_token_response(token_uri, + body='grant_type=authorization_code&code=%s' % code) + self.assertEqual(s, 200) + self.assertEqual(self.validator.confirm_redirect_uri.call_args[0][2], self.DEFAULT_REDIRECT_URI) From 79962015ab8d020a390aa4872777efcc727f5440 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 30 Jul 2018 14:49:44 +0200 Subject: [PATCH 056/237] confirm_r. is called after auth_client --- tests/oauth2/rfc6749/endpoints/test_error_responses.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/oauth2/rfc6749/endpoints/test_error_responses.py b/tests/oauth2/rfc6749/endpoints/test_error_responses.py index 875b3a54..9f46f340 100644 --- a/tests/oauth2/rfc6749/endpoints/test_error_responses.py +++ b/tests/oauth2/rfc6749/endpoints/test_error_responses.py @@ -237,7 +237,6 @@ def test_unauthorized_client(self): def test_access_denied(self): self.validator.authenticate_client.side_effect = self.set_client - self.validator.confirm_redirect_uri.return_value = False token_uri = 'https://i.b/token' # Authorization code grant _, body, _ = self.web.create_token_response(token_uri, From 0c4ce54b4bbae9fb7eb750d59c268eebe98e4e8a Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 30 Jul 2018 14:50:20 +0200 Subject: [PATCH 057/237] Removed silent output, since tests are not writing output it is useful when using pdb from commandline. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3dded414..ebd90218 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ envlist = py27,py34,py35,py36,pypy,docs [testenv] deps= -rrequirements-test.txt -commands=nosetests --with-coverage --cover-erase --cover-package=oauthlib -w tests +commands=nosetests -s --with-coverage --cover-erase --cover-package=oauthlib -w tests [testenv:py27] deps=unittest2 From 3a769e29c2a94bad3460ab09f748569432257396 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 30 Jul 2018 15:07:05 +0200 Subject: [PATCH 058/237] Add syntax check of get_default_redirect_uri Authorization Code was missing this check, whereas Implicit was checking it. --- .../rfc6749/grant_types/authorization_code.py | 2 ++ .../rfc6749/endpoints/test_error_responses.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 06602634..3d088718 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -312,6 +312,8 @@ def validate_authorization_request(self, request): log.debug('Using default redirect_uri %s.', request.redirect_uri) if not request.redirect_uri: raise errors.MissingRedirectURIError(request=request) + if not is_absolute_uri(request.redirect_uri): + raise errors.InvalidRedirectURIError(request=request) # Then check for normal errors. diff --git a/tests/oauth2/rfc6749/endpoints/test_error_responses.py b/tests/oauth2/rfc6749/endpoints/test_error_responses.py index 875b3a54..de0d8346 100644 --- a/tests/oauth2/rfc6749/endpoints/test_error_responses.py +++ b/tests/oauth2/rfc6749/endpoints/test_error_responses.py @@ -44,6 +44,22 @@ def test_invalid_redirect_uri(self): self.assertRaises(errors.InvalidRedirectURIError, self.mobile.create_authorization_response, uri.format('token'), scopes=['foo']) + def test_invalid_default_redirect_uri(self): + uri = 'https://example.com/authorize?response_type={0}&client_id=foo' + self.validator.get_default_redirect_uri.return_value = "wrong" + + # Authorization code grant + self.assertRaises(errors.InvalidRedirectURIError, + self.web.validate_authorization_request, uri.format('code')) + self.assertRaises(errors.InvalidRedirectURIError, + self.web.create_authorization_response, uri.format('code'), scopes=['foo']) + + # Implicit grant + self.assertRaises(errors.InvalidRedirectURIError, + self.mobile.validate_authorization_request, uri.format('token')) + self.assertRaises(errors.InvalidRedirectURIError, + self.mobile.create_authorization_response, uri.format('token'), scopes=['foo']) + def test_missing_redirect_uri(self): uri = 'https://example.com/authorize?response_type={0}&client_id=foo' From 38467a8a001fdbb5ae5661acfcea4e806b82b2b5 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 6 Aug 2018 19:01:53 +0200 Subject: [PATCH 059/237] Implicit was not converting expires_in into integers --- oauthlib/oauth2/rfc6749/parameters.py | 4 ++++ tests/oauth2/rfc6749/clients/test_mobile_application.py | 2 +- tests/oauth2/rfc6749/test_parameters.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 9ea8c446..c5127e7e 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -279,6 +279,10 @@ def parse_implicit_response(uri, state=None, scope=None): fragment = urlparse.urlparse(uri).fragment params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True)) + for key in ('expires_in',): + if key in params: # cast things to int + params[key] = int(params[key]) + if 'scope' in params: params['scope'] = scope_to_list(params['scope']) diff --git a/tests/oauth2/rfc6749/clients/test_mobile_application.py b/tests/oauth2/rfc6749/clients/test_mobile_application.py index 51e4dab3..622b2753 100644 --- a/tests/oauth2/rfc6749/clients/test_mobile_application.py +++ b/tests/oauth2/rfc6749/clients/test_mobile_application.py @@ -40,7 +40,7 @@ class MobileApplicationClientTest(TestCase): token = { "access_token": "2YotnFZFEjr1zCsicMWpAA", "token_type": "example", - "expires_in": "3600", + "expires_in": 3600, "expires_at": 4600, "scope": scope, "example_parameter": "example_value" diff --git a/tests/oauth2/rfc6749/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py index 6ba98c02..b211d1e3 100644 --- a/tests/oauth2/rfc6749/test_parameters.py +++ b/tests/oauth2/rfc6749/test_parameters.py @@ -86,7 +86,7 @@ def setUp(self): 'access_token': '2YotnFZFEjr1zCsicMWpAA', 'state': state, 'token_type': 'example', - 'expires_in': '3600', + 'expires_in': 3600, 'expires_at': 4600, 'scope': ['abc'] } From d9b3f24b497698eddcbe4a0cc1a009d5aa21e9c7 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 10 Aug 2018 12:24:57 +0200 Subject: [PATCH 060/237] Added access_token as JWT examples, and updated JWT grant section A confusion between JWT as token and as authentication mechanism was introduced long-time back and I tried to make a bit of clarity to not confuse again the newcomers. --- docs/oauth2/grants/jwt.rst | 17 ++-- docs/oauth2/preconfigured_servers.rst | 6 +- docs/oauth2/tokens/bearer.rst | 116 +++++++++++++++++++++++++- docs/oauth2/tokens/tokens.rst | 3 +- 4 files changed, 131 insertions(+), 11 deletions(-) diff --git a/docs/oauth2/grants/jwt.rst b/docs/oauth2/grants/jwt.rst index db653424..2c1c0e29 100644 --- a/docs/oauth2/grants/jwt.rst +++ b/docs/oauth2/grants/jwt.rst @@ -1,7 +1,14 @@ -========== -JWT Tokens -========== +============================================================== +JWT Profile for Client Authentication and Authorization Grants +============================================================== -Not yet implemented. Track progress in `GitHub issue 50`_. +If you're looking at JWT Tokens, please see :doc:`Bearer Tokens ` instead. -.. _`GitHub issue 50`: https://github.com/oauthlib/oauthlib/issues/50 +The JWT Profile `RFC7523`_ implements the `RFC7521`_ abstract assertion +protocol. It aims to extend the OAuth2 protocol to use JWT as an +additional authorization grant. + +Currently, this is not implemented but all PRs are welcome. See how to :doc:`Contribute `. + +.. _`RFC7521`: https://tools.ietf.org/html/rfc7521 +.. _`RFC7523`: https://tools.ietf.org/html/rfc7523 diff --git a/docs/oauth2/preconfigured_servers.rst b/docs/oauth2/preconfigured_servers.rst index 6184c271..e1f629c2 100644 --- a/docs/oauth2/preconfigured_servers.rst +++ b/docs/oauth2/preconfigured_servers.rst @@ -12,7 +12,8 @@ Construction is simple, only import your validator and you are good to go:: server = WebApplicationServer(your_validator) -If you prefer to construct tokens yourself you may pass a token generator:: +If you prefer to construct tokens yourself you may pass a token generator (see + :doc:`Tokens ` for more examples like JWT) :: def your_token_generator(request, refresh_token=False): return 'a_custom_token' + request.client_id @@ -21,6 +22,9 @@ If you prefer to construct tokens yourself you may pass a token generator:: This function is passed the request object and a boolean indicating whether to generate an access token (False) or a refresh token (True). +.. autoclass:: oauthlib.oauth2.Server + :members: + .. autoclass:: oauthlib.oauth2.WebApplicationServer :members: diff --git a/docs/oauth2/tokens/bearer.rst b/docs/oauth2/tokens/bearer.rst index 8c6270d1..0776db8b 100644 --- a/docs/oauth2/tokens/bearer.rst +++ b/docs/oauth2/tokens/bearer.rst @@ -2,12 +2,122 @@ Bearer Tokens ============= -The most common OAuth 2 token type. It provides very little in terms of security -and relies heavily upon the ability of the client to keep the token secret. +The most common OAuth 2 token type. -Bearer tokens are the default setting with all configured endpoints. Generally +Bearer tokens is the default setting for all configured endpoints. Generally you will not need to ever construct a token yourself as the provided servers will do so for you. +By default, :doc:`*Server ` generate Bearer tokens as +random strings. However, you can change the default behavior to generate JWT +instead. All preconfigured servers take as parameters `token_generator` and +`refresh_token_generator` to fit your needs. + +.. contents:: Tutorial Contents + :depth: 3 + + +1. Generate signed JWT +---------------------- + +A function is available to generate signed JWT (with RS256 PEM key) with static +and dynamic claims. + +.. code-block:: python + + from oauthlib.oauth2.rfc6749 import tokens + from oauthlib.oauth2 import Server + + private_pem_key = + validator = + + server = Server( + your_validator, + token_generator=tokens.signed_token_generator(private_pem_key, issuer="foobar") + ) + + +Note that you can add any custom claims in `RequestValidator` methods by adding them to +`request.claims` dictionary. Example below: + + +.. code-block:: python + + def validate_client_id(self, client_id, request): + (.. your usual checks ..) + + request.claims = { + 'aud': self.client_id + } + return True + + +Once completed, the token endpoint will generate access_token in JWT form: + +.. code-block:: shell + + + access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJy(..)&expires_in=120&token_type=Bearer(..) + + +And you will find all claims in its decoded form: + + +.. code-block:: javascript + + { + "aud": "", + "iss": "foobar", + "scope": "profile calendar", + "exp": 12345, + } + + +2. Define your own implementation (text, JWT, JWE, ...) +---------------------------------------------------------------- + +Sometime you may want to generate custom `access_token` with a reference from a +database (as text) or use a HASH signature in JWT or use JWE (encrypted content). + +Also, note that you can declare the generate function in your instanciated +validator to benefit of the `self` variables. + +See the example below: + +.. code-block:: python + + class YourValidator(RequestValidator): + def __init__(self, secret, issuer): + self.secret = secret + self.issuer = issuer + + def generate_access_token(self, request): + token = jwt.encode({ + "ref": str(libuuid.uuid4()), + "aud": request.client_id, + "iss": self.issuer, + "exp": now + datetime.timedelta(seconds=request.expires_in) + }, self.secret, algorithm='HS256').decode() + return token + + +Then associate it to your `Server`: + +.. code-block:: python + + validator = YourValidator(secret="", issuer="") + + server = Server( + your_validator, + token_generator=validator.generate_access_token + ) + + +3. BearerToken API +------------------ + +If none of the :doc:`/oauth2/preconfigured_servers` fit your needs, you can +declare your own Endpoints and use the `BearerToken` API as below. + .. autoclass:: oauthlib.oauth2.BearerToken :members: diff --git a/docs/oauth2/tokens/tokens.rst b/docs/oauth2/tokens/tokens.rst index f3415097..4e19e7e6 100644 --- a/docs/oauth2/tokens/tokens.rst +++ b/docs/oauth2/tokens/tokens.rst @@ -3,8 +3,7 @@ Tokens ====== The main token type of OAuth 2 is Bearer tokens and that is what OAuthLib -currently supports. Other tokens, such as JWT, SAML and possibly MAC (if the -spec matures) can easily be added (and will be in due time). +currently supports. Other tokens, such as SAML and MAC can easily be added. The purpose of a token is to authorize access to protected resources to a client (i.e. your G+ feed). From 9abadd701ce46b9df548a30d58fe13a636b222e5 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 20 Mar 2018 09:45:29 +0100 Subject: [PATCH 061/237] Added emacs ignore list --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 683f357b..6f246494 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,8 @@ htmlcov* # Local file cruft/auto-backups .DS_Store *~ +**/#*# +**/.#* # Sphinx docs/_build From a03cb7321d583897ac9865d9cb24b632081d16b1 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 20 Mar 2018 09:44:27 +0100 Subject: [PATCH 062/237] Added README.rst/setup.py check for pypi --- tox.ini | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8f3345eb..03e25b12 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36,pypy,docs +envlist = py27,py34,py35,py36,pypy,docs,readme [testenv] deps= @@ -19,3 +19,12 @@ deps=sphinx changedir=docs whitelist_externals=make commands=make clean html + +# tox -e readme to mimick pypi long_description check +[testenv:readme] +skipsdist=True +deps=readme +whitelist_externals=echo +commands= + python setup.py check -r -s + echo setup.py/long description is syntaxly correct From 19ebeae8619c7c784427acf3d37667e403564780 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 20 Mar 2018 09:43:34 +0100 Subject: [PATCH 063/237] Added credits to Idan & team. --- LICENSE | 4 ++-- README.rst | 12 ++++++++++++ docs/conf.py | 2 +- oauthlib/__init__.py | 2 +- setup.py | 2 +- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/LICENSE b/LICENSE index c10d2560..84b5c756 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011 Idan Gazit and contributors +Copyright (c) 2018 The OAuthlib Community All rights reserved. Redistribution and use in source and binary forms, with or without @@ -24,4 +24,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.rst b/README.rst index 6741a758..394a984d 100644 --- a/README.rst +++ b/README.rst @@ -104,6 +104,18 @@ License OAuthLib is yours to use and abuse according to the terms of the BSD license. Check the LICENSE file for full details. +Credits +------- + +OAuthLib has been started and maintained several years by Idan Gazit and other +amazing `AUTHORS`_. Thanks to their wonderful work, the open-source `community`_ +creation has been possible and the project can stay active and reactive to users +requests. + + +.. _`AUTHORS`: https://github.com/oauthlib/oauthlib/blob/master/AUTHORS +.. _`community`: https://github.com/oauthlib/ + Changelog --------- diff --git a/docs/conf.py b/docs/conf.py index 017f6861..2594e387 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,7 +41,7 @@ # General information about the project. project = u'OAuthLib' -copyright = u'2012, Idan Gazit and the Python Community' +copyright = u'2018, The OAuthlib Community' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index b7586d2b..bc5d96b5 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -5,7 +5,7 @@ A generic, spec-compliant, thorough implementation of the OAuth request-signing logic. - :copyright: (c) 2011 by Idan Gazit. + :copyright: (c) 2018 by The OAuthlib Community :license: BSD, see LICENSE for details. """ import logging diff --git a/setup.py b/setup.py index 0c4e5642..1d69e0d9 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def fread(fn): version=oauthlib.__version__, description='A generic, spec-compliant, thorough implementation of the OAuth request-signing logic', long_description=fread('README.rst'), - author='Idan Gazit', + author='The OAuthlib Community', author_email='idan@gazit.me', maintainer='Ib Lundgren', maintainer_email='ib.lundgren@gmail.com', From 7b2de403d591ddf70f6e9d21d4c5f55edad7642d Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 19 Mar 2018 16:27:45 +0100 Subject: [PATCH 064/237] Fixed typo --- docs/release_process.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/release_process.rst b/docs/release_process.rst index aab97c4f..9ee987c2 100644 --- a/docs/release_process.rst +++ b/docs/release_process.rst @@ -2,12 +2,12 @@ Release process =============== OAuthLib has got to a point where quite a few libraries and users depend on it. -Because of this a more careful release procedure will be introduced to make +Because of this, a more careful release procedure will be introduced to make sure all these lovely projects don't suddenly break. When approaching a release we will run the unittests for a set of downstream libraries using the unreleased version of OAuthLib. If OAuthLib is the cause of -failing tests we will either +failing tests we will either: 1. Find a way to introduce the change without breaking downstream. However, this is not always the best long term option. @@ -25,7 +25,7 @@ OAuthLib release issue on Github at least 2 days prior to release detailing the changes and pings the primary contacts for each downstream project. Please respond within those 2 days if you have major concerns. -How to get on the notifcations list +How to get on the notifications list ----------------------------------- Which projects and the instructions for testing each will be defined in @@ -45,8 +45,8 @@ A note on versioning -------------------- Historically OAuthLib has not been very good at semantic versioning but that -will change after the 1.0.0 release due late 2014. After that poing any major -digit release (e.g. 2.0.0) may introduce non backwards compatible changes. +has changed since the 1.0.0 in 2014. Since, any major digit release +(e.g. 2.0.0) may introduce non backwards compatible changes. Minor point (1.1.0) releases will introduce non API breaking new features and changes. Bug releases (1.0.1) will include minor fixes that needs to be released quickly (e.g. after a bigger release unintentionally introduced a From e18a4cf83745642080fa0a2233959cc52617c679 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 19 Mar 2018 16:27:33 +0100 Subject: [PATCH 065/237] Removed last occurences of G+ with Gitter --- docs/faq.rst | 6 +++--- docs/oauth1/server.rst | 4 ++-- docs/oauth2/server.rst | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 38b0e92a..04c59ec2 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -72,8 +72,8 @@ How do I use OAuthlib as a provider with Django, Flask and other web frameworks? - Pyramid `pyramid-oauthlib`_ - Bottle `bottle-oauthlib`_ - For other frameworks, please get in touch by opening a `GitHub issue`_, on `G+`_ or - on IRC #oauthlib irc.freenode.net. If you have written an OAuthLib package that + For other frameworks, please get in touch by opening a `GitHub issue`_ or + on `Gitter OAuthLib community`_. If you have written an OAuthLib package that supports your favorite framework, please open a Pull Request to update the docs. @@ -101,5 +101,5 @@ Some argue OAuth 2 is worse than 1, is that true? .. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib .. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib .. _`GitHub issue`: https://github.com/oauthlib/oauthlib/issues/new -.. _`G+`: https://plus.google.com/communities/101889017375384052571 +.. _`Gitter OAuthLib community`: https://gitter.im/oauthlib/Lobby .. _`difference`: https://www.cyberciti.biz/faq/authentication-vs-authorization/ diff --git a/docs/oauth1/server.rst b/docs/oauth1/server.rst index 2a91f302..db469d2c 100644 --- a/docs/oauth1/server.rst +++ b/docs/oauth1/server.rst @@ -433,9 +433,9 @@ shown below as well as run your flask server locally on port `5000`. 7. Let us know how it went! --------------------------- -Drop a line in our `G+ community`_ or open a `GitHub issue`_ =) +Drop a line in our `Gitter OAuthLib community`_ or open a `GitHub issue`_ =) -.. _`G+ community`: https://plus.google.com/communities/101889017375384052571 +.. _`Gitter OAuthLib community`: https://gitter.im/oauthlib/Lobby .. _`GitHub issue`: https://github.com/oauthlib/oauthlib/issues/new If you run into issues it can be helpful to enable debug logging:: diff --git a/docs/oauth2/server.rst b/docs/oauth2/server.rst index 8f8b77bc..35a58aaf 100644 --- a/docs/oauth2/server.rst +++ b/docs/oauth2/server.rst @@ -493,9 +493,9 @@ at runtime by a function, rather then by a list. 6. Let us know how it went! --------------------------- -Drop a line in our `G+ community`_ or open a `GitHub issue`_ =) +Drop a line in our `Gitter OAuthLib community`_ or open a `GitHub issue`_ =) -.. _`G+ community`: https://plus.google.com/communities/101889017375384052571 +.. _`Gitter OAuthLib community`: https://gitter.im/oauthlib/Lobby .. _`GitHub issue`: https://github.com/oauthlib/oauthlib/issues/new If you run into issues it can be helpful to enable debug logging. From ed1a02442a20c82976bc84698c9a3f68e9d4631e Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 19 Mar 2018 16:27:12 +0100 Subject: [PATCH 066/237] Mention our "extra" flags somewhere. --- docs/faq.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index 04c59ec2..d9cd5c60 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -14,19 +14,23 @@ What parts of OAuth 1 & 2 are supported? OAuth 1 with RSA-SHA1 signatures says "could not import cryptography". What should I do? ---------------------------------------------------------------------------------- - Install cryptography via pip. + Install oauthlib with rsa flag or install cryptography manually via pip. .. code-block:: sh + $ pip install oauthlib[rsa] + ..or.. $ pip install cryptography OAuth 2 ServiceApplicationClient and OAuth 1 with RSA-SHA1 signatures say "could not import jwt". What should I do? ------------------------------------------------------------------------------------------------------------------- - Install pyjwt and cryptography with pip. + Install oauthlib with signedtoken flag or install pyjwt and cryptography manually with pip. .. code-block:: sh + $ pip install oauthlib[signedtoken] + ..or.. $ pip install pyjwt cryptography What does ValueError `Only unicode objects are escapable. Got one of type X.` mean? From 28d95db162aebfbdb6872141c70acf05e7fa2c20 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 19 Mar 2018 16:26:56 +0100 Subject: [PATCH 067/237] Added upstream test as best practice --- docs/contributing.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/contributing.rst b/docs/contributing.rst index 601c5670..3c05f45c 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -150,6 +150,17 @@ version. For Ubuntu you can easily install all after adding one ppa. .. _`Tox`: https://tox.readthedocs.io/en/latest/install.html .. _`virtualenv`: https://virtualenv.pypa.io/en/latest/installation/ +Test upstream applications +----------------------------------- + +Remember, OAuthLib is used by several 3rd party projects. If you think you +submit a breaking change, confirm that other projects builds are not affected. + +.. sourcecode:: bash + + $ make + + If you add code you need to add tests! -------------------------------------- From 6f4fbe666de3aaf6f3fca22705262665b4c23173 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 19 Mar 2018 16:26:38 +0100 Subject: [PATCH 068/237] Updated python versions --- docs/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 3c05f45c..d3d9a396 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -127,7 +127,7 @@ request that fails this test suite will be **rejected**. Testing multiple versions of Python ----------------------------------- -OAuthLib supports Python 2.6, 2.7, 3.2, 3.3 and experimentally PyPy. Testing +OAuthLib supports Python 2.7, 3.4, 3.5, 3.6 and PyPy. Testing all versions conveniently can be done using `Tox`_. .. sourcecode:: bash From e53203aa1a9daf9d5af1607900dc3d8b26a81aa0 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 19 Mar 2018 16:25:17 +0100 Subject: [PATCH 069/237] Fixed bad copy/paste --- docs/contributing.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index d3d9a396..3a23d70c 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -35,7 +35,7 @@ Setting up topic branches and generating pull requests While it's handy to provide useful code snippets in an issue, it is better for you as a developer to submit pull requests. By submitting pull request your -contribution to OpenComparison will be recorded by Github. +contribution to OAuthlib will be recorded by Github. In git it is best to isolate each topic or feature into a "topic branch". While individual commits allow you control over how small individual changes are made @@ -218,7 +218,7 @@ How pull requests are checked, tested, and done First we pull the code into a local branch:: - git remote add git@github.com:/opencomparison.git + git remote add git@github.com:/oauthlib.git git fetch git checkout -b / From d5d843de71e2ddd6da913971f42beec890f5c3b7 Mon Sep 17 00:00:00 2001 From: Chris Utz Date: Sun, 12 Aug 2018 15:44:38 -0500 Subject: [PATCH 070/237] $ and ' are allowed to be unencoded in query strings (#564) --- oauthlib/common.py | 2 +- tests/test_common.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/oauthlib/common.py b/oauthlib/common.py index f25656ff..c1180e62 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -114,7 +114,7 @@ def decode_params_utf8(params): return decoded -urlencoded = set(always_safe) | set('=&;:%+~,*@!()/?') +urlencoded = set(always_safe) | set('=&;:%+~,*@!()/?\'$') def urldecode(query): diff --git a/tests/test_common.py b/tests/test_common.py index b0ea20d3..fb4bd5b2 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -39,6 +39,8 @@ def test_urldecode(self): self.assertItemsEqual(urldecode('foo=bar@spam'), [('foo', 'bar@spam')]) self.assertItemsEqual(urldecode('foo=bar/baz'), [('foo', 'bar/baz')]) self.assertItemsEqual(urldecode('foo=bar?baz'), [('foo', 'bar?baz')]) + self.assertItemsEqual(urldecode('foo=bar\'s'), [('foo', 'bar\'s')]) + self.assertItemsEqual(urldecode('foo=$'), [('foo', '$')]) self.assertRaises(ValueError, urldecode, 'foo bar') self.assertRaises(ValueError, urldecode, '%R') self.assertRaises(ValueError, urldecode, '%RA') From 1711cdb758ee6c316f001e4e49746efb9f1cd449 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 13 Aug 2018 00:31:46 +0200 Subject: [PATCH 071/237] Add NCoC and Code of merit --- CODE_OF_CONDUCT.md | 5 +++++ docs/contributing.rst | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..a734e49f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,5 @@ +# Contributor Code of Conduct + +This project adheres to No Code of Conduct. We are all adults. We accept anyone's contributions. Nothing else matters. + +For more information please visit the [No Code of Conduct](https://github.com/domgetter/NCoC) homepage. diff --git a/docs/contributing.rst b/docs/contributing.rst index 3a23d70c..70614dd6 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -30,6 +30,28 @@ personal label matching your GitHub ID will be assigned to that issue. Feel free to propose issues that aren't described! +oauthlib community rules +======================== + +oauthlib is a community of developers which adheres to a very simple set of +rules. + +Code of Conduct +--------------- +This project adheres to No Code of Conduct. We are all adults. We accept +anyone's contributions. Nothing else matters. +For more information please visit the `No Code of Conduct`_ homepage. + +.. _`No Code of Conduct`: https://github.com/domgetter/NCoC + +Code of Merit +------------- +Please read the community's `Code of Merit`_. Every contributor will know the +real purpose of their contributions to this project. + +.. _`Code of Merit`: http://code-of-merit.org/ + + Setting up topic branches and generating pull requests ====================================================== From 3faf434e8d670bf2763bbdc5135cbd7e747194f8 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 15 Aug 2018 00:12:20 +0200 Subject: [PATCH 072/237] Restore confirm = False test --- tests/oauth2/rfc6749/endpoints/test_error_responses.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/oauth2/rfc6749/endpoints/test_error_responses.py b/tests/oauth2/rfc6749/endpoints/test_error_responses.py index 9f46f340..677b8959 100644 --- a/tests/oauth2/rfc6749/endpoints/test_error_responses.py +++ b/tests/oauth2/rfc6749/endpoints/test_error_responses.py @@ -237,6 +237,8 @@ def test_unauthorized_client(self): def test_access_denied(self): self.validator.authenticate_client.side_effect = self.set_client + self.validator.get_default_redirect_uri.return_value = 'https://i.b/cb' + self.validator.confirm_redirect_uri.return_value = False token_uri = 'https://i.b/token' # Authorization code grant _, body, _ = self.web.create_token_response(token_uri, From 058746b3d9bed4aafbd55a7f26491b5761c35fa8 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 15 Aug 2018 00:15:40 +0200 Subject: [PATCH 073/237] Add test when no redirecturi & no default --- tests/oauth2/rfc6749/endpoints/test_error_responses.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/oauth2/rfc6749/endpoints/test_error_responses.py b/tests/oauth2/rfc6749/endpoints/test_error_responses.py index 677b8959..00f7ba6a 100644 --- a/tests/oauth2/rfc6749/endpoints/test_error_responses.py +++ b/tests/oauth2/rfc6749/endpoints/test_error_responses.py @@ -245,6 +245,15 @@ def test_access_denied(self): body='grant_type=authorization_code&code=foo') self.assertEqual('invalid_request', json.loads(body)['error']) + def test_access_denied_no_default_redirecturi(self): + self.validator.authenticate_client.side_effect = self.set_client + self.validator.get_default_redirect_uri.return_value = None + token_uri = 'https://i.b/token' + # Authorization code grant + _, body, _ = self.web.create_token_response(token_uri, + body='grant_type=authorization_code&code=foo') + self.assertEqual('invalid_request', json.loads(body)['error']) + def test_unsupported_response_type(self): self.validator.get_default_redirect_uri.return_value = 'https://i.b/cb' From 97debbc56083950ff1940de43d7fa89c5ed1abbd Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 16 Aug 2018 01:29:42 +0200 Subject: [PATCH 074/237] client_id is not passed to save_bearer_token --- oauthlib/oauth2/rfc6749/request_validator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index bf1515dc..ff3bbd64 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -346,7 +346,6 @@ def save_bearer_token(self, token, request, *args, **kwargs): the claims dict, which should be saved for later use when generating the id_token and/or UserInfo response content. - :param client_id: Unicode client identifier :param token: A Bearer token dict :param request: The HTTP Request (oauthlib.common.Request) :rtype: The default redirect URI for the client From ff40476151e46f162459bca9270984135ca5ef3b Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 17 Aug 2018 01:04:46 +0200 Subject: [PATCH 075/237] Replaced NCoC with Django CoC --- CODE_OF_CONDUCT.md | 29 ++++++++++++++++++++++++++--- docs/contributing.rst | 11 +++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index a734e49f..3f242ff1 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,5 +1,28 @@ -# Contributor Code of Conduct +# OAuthlib Code of Conduct -This project adheres to No Code of Conduct. We are all adults. We accept anyone's contributions. Nothing else matters. +Like the technical community as a whole, the OAuthlib team and community is made up of a mixture of professionals and volunteers from all over the world, working on every aspect of the mission - including mentorship, teaching, and connecting people. -For more information please visit the [No Code of Conduct](https://github.com/domgetter/NCoC) homepage. +Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to founders, mentors and those seeking help and guidance. + +This isn't an exhaustive list of things that you can't do. Rather, take it in the spirit in which it's intended - a guide to make it easier to enrich all of us and the technical communities in which we participate. + +This code of conduct applies to all spaces managed by the OAuthlib project. This includes Gitter, the mailing lists, the issue tracker, and any other forums created by the project team which the community uses for communication. In addition, violations of this code outside these spaces may affect a person's ability to participate within them. + +If you believe someone is violating the code of conduct, we ask that you report it by contacting us. + + Be friendly and patient. + Be welcoming. We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. + Be considerate. Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. + Be respectful. Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It's important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the OAuthlib community should be respectful when dealing with other members as well as with people outside the OAuthlib community. + Be careful in the words that you choose. We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to: + Violent threats or language directed against another person. + Discriminatory jokes and language. + Posting sexually explicit or violent material. + Posting (or threatening to post) other people's personally identifying information ("doxing"). + Personal insults, especially those using racist or sexist terms. + Unwelcome sexual attention. + Advocating for, or encouraging, any of the above behavior. + Repeated harassment of others. In general, if someone asks you to stop, then stop. + When we disagree, try to understand why. Disagreements, both social and technical, happen all the time and OAuthlib is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we're different. The strength of OAuthlib comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn't mean that they're wrong. Don't forget that it is human to err and blaming each other doesn't get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. + +For reading the original text, please visit the [Django Code of Conduct](https://www.djangoproject.com/conduct/). diff --git a/docs/contributing.rst b/docs/contributing.rst index 70614dd6..cbdb5194 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -38,11 +38,14 @@ rules. Code of Conduct --------------- -This project adheres to No Code of Conduct. We are all adults. We accept -anyone's contributions. Nothing else matters. -For more information please visit the `No Code of Conduct`_ homepage. +This project adheres to a `Code of Conduct`_ based on Django. As a community +member you have to read and agree with it. -.. _`No Code of Conduct`: https://github.com/domgetter/NCoC +For more information please contact us and/or visit the original +`Django Code of Conduct`_ homepage. + +.. _`Code of Conduct`: https://github.com/oauthlib/oauthlib/blob/master/CODE_OF_CONDUCT.md +.. _`Django Code of Conduct`: https://www.djangoproject.com/conduct/ Code of Merit ------------- From 6b00071b793067dfff8b42391692123023966075 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sat, 18 Aug 2018 00:10:40 +0200 Subject: [PATCH 076/237] Change sentences for better SEO --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 394a984d..03129f7a 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -OAuthLib -======== +OAuthLib - Python Framework for OAuth1 & OAuth2 +=============================================== *A generic, spec-compliant, thorough implementation of the OAuth request-signing logic for Python 2.7 and 3.4+.* @@ -34,7 +34,7 @@ both of the following: .. _`OAuth 1.0 spec`: https://tools.ietf.org/html/rfc5849 .. _`OAuth 2.0 spec`: https://tools.ietf.org/html/rfc6749 -OAuthLib is a generic utility which implements the logic of OAuth without +OAuthLib is a framework which implements the logic of OAuth1 or OAuth2 without assuming a specific HTTP request object or web framework. Use it to graft OAuth client support onto your favorite HTTP library, or provide support onto your favourite web framework. If you're a maintainer of such a library, write a thin @@ -119,7 +119,7 @@ requests. Changelog --------- -*OAuthLib is in active development, with the core of both OAuth 1 and 2 +*OAuthLib is in active development, with the core of both OAuth1 and OAuth2 completed, for providers as well as clients.* See `supported features`_ for details. From 39dad84a9b4cfa353ec3ed60aa8f8856957f6704 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Sat, 18 Aug 2018 01:02:52 +0200 Subject: [PATCH 077/237] Remove headers from request attributes --- oauthlib/common.py | 1 - tests/test_common.py | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/oauthlib/common.py b/oauthlib/common.py index c1180e62..63647616 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -426,7 +426,6 @@ def __init__(self, uri, http_method='GET', body=None, headers=None, } self._params.update(dict(urldecode(self.uri_query))) self._params.update(dict(self.decoded_body or [])) - self._params.update(self.headers) def __getattr__(self, name): if name in self._params: diff --git a/tests/test_common.py b/tests/test_common.py index fb4bd5b2..f239368d 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -214,6 +214,11 @@ def test_password_body(self): self.assertNotIn('bar', repr(r)) self.assertIn('', repr(r)) + def test_headers_params(self): + r = Request(URI, headers={'token': 'foobar'}, body='token=banana') + self.assertEqual(r.headers['token'], 'foobar') + self.assertEqual(r.token, 'banana') + class CaseInsensitiveDictTest(TestCase): From 698f59eeff2307d02c621a92f3d58936bc524469 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 20 Aug 2018 00:21:26 +0200 Subject: [PATCH 078/237] Initial issue templates See #541 --- .github/ISSUE_TEMPLATE/bug_report.md | 21 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 14 ++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..9c82293a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Context** +Are you using OAuth1 ? OAuth2 ? +Using a client ? a public provider ? +Implementing your own provider ? +Using a downstream library ? (requests-oauthlib, django-oauth-toolkit ...) +Add any other context. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..81e6b068 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context or screenshots about the feature request here. From f3d3eb9efd81459be48b052e172ffa5f76a7a445 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 29 Aug 2018 14:28:39 +0300 Subject: [PATCH 079/237] Added license check badge. (#581) Since we're used in enterprises where licensing matters, this may be useful when people evaluate our project. --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 03129f7a..a84307f2 100644 --- a/README.rst +++ b/README.rst @@ -16,6 +16,9 @@ logic for Python 2.7 and 3.4+.* .. image:: https://img.shields.io/pypi/l/oauthlib.svg :target: https://pypi.org/project/oauthlib/ :alt: License +.. image:: https://app.fossa.io/api/projects/git%2Bgithub.com%2Foauthlib%2Foauthlib.svg?type=shield + :target: https://app.fossa.io/projects/git%2Bgithub.com%2Foauthlib%2Foauthlib?ref=badge_shield + :alt: FOSSA Status .. image:: https://img.shields.io/readthedocs/oauthlib.svg :target: https://oauthlib.readthedocs.io/en/latest/index.html :alt: Read the Docs From a839bc209c28721ff10bc3b55e065ab7e0a84931 Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 30 Aug 2018 10:03:56 -0600 Subject: [PATCH 080/237] Mention `oauth_body_hash` in OAuth1 client docs While the previous documentation was not wrong in that non-formencoded data are not included in the signature for traditional OAuth1 service providers, the library does still include an `oauth_body_hash` for non-formencoded data. Update the documentation to include mention of the `oauth_body_hash` with a notice that validation of said parameter may not be supported by all service providers, but will nevertheless provide an additional integrity check for those that do support it. --- docs/oauth1/client.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/oauth1/client.rst b/docs/oauth1/client.rst index 741374ea..ec6bdd74 100644 --- a/docs/oauth1/client.rst +++ b/docs/oauth1/client.rst @@ -52,15 +52,23 @@ Using the Client **Request body** The OAuth 1 spec only covers signing of x-www-url-formencoded information. - If you are sending some other kind of data in the body (say, multipart file - uploads), these don't count as a body for the purposes of signing. Don't - provide the body to Client.sign() if it isn't x-www-url-formencoded data. For convenience, you can pass body data in one of three ways: * a dictionary * an iterable of 2-tuples * a properly-formatted x-www-url-formencoded string + + If you are sending some other kind of data in the body, an additional + `oauth_body_hash` parameter will be included with the request. This parameter + provides an integrity check on non-formencoded request bodies. + + *IMPORTANT* This extension is forward compatible: Service Providers that + have not implemented this extension can verify requests sent by Consumers + that have implemented this extension. If the Service Provider implements + this specification the integrity of the body is guaranteed. If the + Service Provider does not check body signatures, the remainder of the + request will still validate using the OAuth Core signature algorithm. **RSA Signatures** From 997e8d061ae883a6460aeda71ab12b2b5bd4feed Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 2 Sep 2018 10:13:08 -0700 Subject: [PATCH 081/237] Make scope optional for authorization code grant. --- .../oauth2/rfc6749/grant_types/authorization_code.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 43d2efab..ab4c1840 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -140,7 +140,6 @@ def create_authorization_response(self, request, token_handler): oauthlib.oauth2.BearerToken. :returns: headers, body, status :raises: FatalClientError on invalid redirect URI or client id. - ValueError if scopes are not set on the request object. A few examples:: @@ -151,12 +150,6 @@ def create_authorization_response(self, request, token_handler): >>> from oauthlib.oauth2 import AuthorizationCodeGrant, BearerToken >>> token = BearerToken(your_validator) >>> grant = AuthorizationCodeGrant(your_validator) - >>> grant.create_authorization_response(request, token) - Traceback (most recent call last): - File "", line 1, in - File "oauthlib/oauth2/rfc6749/grant_types.py", line 513, in create_authorization_response - raise ValueError('Scopes must be set on post auth.') - ValueError: Scopes must be set on post auth. >>> request.scopes = ['authorized', 'in', 'some', 'form'] >>> grant.create_authorization_response(request, token) (u'http://client.com/?error=invalid_request&error_description=Missing+response_type+parameter.', None, None, 400) @@ -182,11 +175,6 @@ def create_authorization_response(self, request, token_handler): .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12 """ try: - # request.scopes is only mandated in post auth and both pre and - # post auth use validate_authorization_request - if not request.scopes: - raise ValueError('Scopes must be set on post auth.') - self.validate_authorization_request(request) log.debug('Pre resource owner authorization validation ok for %r.', request) From f7df56a9286b3fd06d636ef43ab3d4a4c86c1918 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Sun, 2 Sep 2018 10:52:18 -0700 Subject: [PATCH 082/237] Fix test_error_catching. --- tests/oauth2/rfc6749/endpoints/test_base_endpoint.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py index 4ad0ed96..4f78d9b3 100644 --- a/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py @@ -24,7 +24,9 @@ def test_error_catching(self): validator = RequestValidator() server = Server(validator) server.catch_errors = True - h, b, s = server.create_authorization_response('https://example.com') + h, b, s = server.create_token_response( + 'https://example.com?grant_type=authorization_code&code=abc' + ) self.assertIn("server_error", b) self.assertEqual(s, 500) From fd5c9790e8219fdc6a85b4837ba4f5a2eb265d09 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Mon, 3 Sep 2018 22:19:30 -0700 Subject: [PATCH 083/237] Write a test for authorization grant w/ no scope. --- tests/oauth2/rfc6749/grant_types/test_authorization_code.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/oauth2/rfc6749/grant_types/test_authorization_code.py b/tests/oauth2/rfc6749/grant_types/test_authorization_code.py index 704a254a..acb23acd 100644 --- a/tests/oauth2/rfc6749/grant_types/test_authorization_code.py +++ b/tests/oauth2/rfc6749/grant_types/test_authorization_code.py @@ -77,6 +77,12 @@ def test_create_authorization_grant(self): self.assertTrue(self.mock_validator.validate_response_type.called) self.assertTrue(self.mock_validator.validate_scopes.called) + def test_create_authorization_grant_no_scopes(self): + bearer = BearerToken(self.mock_validator) + self.request.response_mode = 'query' + self.request.scopes = [] + self.auth.create_authorization_response(self.request, bearer) + def test_create_authorization_grant_state(self): self.request.state = 'abc' self.request.redirect_uri = None From e81ae772e4f260cc02ce07a7396470821ac63b1e Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 2 Aug 2018 00:54:54 +0200 Subject: [PATCH 084/237] Add support of custom errors coming from providers Fix #431. The inherent function "raise_from_error" is called when "error=" is found in the payload. So it MUST raise something, and until now, only RFC errors were raised. --- oauthlib/oauth2/rfc6749/errors.py | 12 ++++++++++++ tests/oauth2/rfc6749/test_parameters.py | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 5a0cca2b..8882ab22 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -313,6 +313,7 @@ class ConsentRequired(OAuth2Error): error = 'consent_required' status_code = 401 + class LoginRequired(OAuth2Error): """ The Authorization Server requires End-User authentication. @@ -325,6 +326,16 @@ class LoginRequired(OAuth2Error): status_code = 401 +class CustomOAuth2Error(OAuth2Error): + """ + This error is a placeholder for all custom errors not described by the RFC. + Some of the popular OAuth2 providers are using custom errors. + """ + def __init__(self, error, *args, **kwargs): + self.error = error + super().__init__(*args, **kwargs) + + def raise_from_error(error, params=None): import inspect import sys @@ -336,3 +347,4 @@ def raise_from_error(error, params=None): for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): if cls.error == error: raise cls(**kwargs) + raise CustomOAuth2Error(error=error, **kwargs) diff --git a/tests/oauth2/rfc6749/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py index b211d1e3..c42f516c 100644 --- a/tests/oauth2/rfc6749/test_parameters.py +++ b/tests/oauth2/rfc6749/test_parameters.py @@ -103,6 +103,7 @@ def setUp(self): ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",' ' "example_parameter": "example_value" }') + json_custom_error = '{ "error": "incorrect_client_credentials" }' json_error = '{ "error": "access_denied" }' json_notoken = ('{ "token_type": "example",' @@ -197,6 +198,9 @@ def test_implicit_token_response(self): self.assertRaises(ValueError, parse_implicit_response, self.implicit_wrongstate, state=self.state) + def test_custom_json_error(self): + self.assertRaises(CustomOAuth2Error, parse_token_response, self.json_custom_error) + def test_json_token_response(self): """Verify correct parameter parsing and validation for token responses. """ self.assertEqual(parse_token_response(self.json_response), self.json_dict) From 346bf28816da5705e7681a886247c0f32884723b Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 2 Aug 2018 10:04:13 +0200 Subject: [PATCH 085/237] Fixed py27/pypy support --- oauthlib/oauth2/rfc6749/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 8882ab22..a15d6c54 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -333,7 +333,7 @@ class CustomOAuth2Error(OAuth2Error): """ def __init__(self, error, *args, **kwargs): self.error = error - super().__init__(*args, **kwargs) + super(CustomOAuth2Error, self).__init__(*args, **kwargs) def raise_from_error(error, params=None): From 5a9d8d92d3453355de86d614337affe69543207d Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Mon, 10 Sep 2018 17:00:16 -0400 Subject: [PATCH 086/237] redid the docstring fixes --- .../oauth1/rfc5849/endpoints/access_token.py | 6 +- .../oauth1/rfc5849/endpoints/authorization.py | 3 +- .../oauth1/rfc5849/endpoints/request_token.py | 6 +- oauthlib/oauth1/rfc5849/request_validator.py | 63 ++++--- .../rfc6749/grant_types/authorization_code.py | 23 ++- oauthlib/oauth2/rfc6749/grant_types/base.py | 35 +++- .../rfc6749/grant_types/client_credentials.py | 9 + .../oauth2/rfc6749/grant_types/implicit.py | 18 ++ .../rfc6749/grant_types/refresh_token.py | 9 + .../resource_owner_password_credentials.py | 8 + oauthlib/oauth2/rfc6749/parameters.py | 25 +-- oauthlib/oauth2/rfc6749/request_validator.py | 167 +++++++++++------- oauthlib/oauth2/rfc6749/tokens.py | 53 +++++- .../openid/connect/core/request_validator.py | 21 ++- 14 files changed, 331 insertions(+), 115 deletions(-) diff --git a/oauthlib/oauth1/rfc5849/endpoints/access_token.py b/oauthlib/oauth1/rfc5849/endpoints/access_token.py index 12d13e9b..bea82741 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/access_token.py +++ b/oauthlib/oauth1/rfc5849/endpoints/access_token.py @@ -37,7 +37,8 @@ def create_access_token(self, request, credentials): Similar to OAuth 2, indication of granted scopes will be included as a space separated list in ``oauth_authorized_realms``. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: The token as an urlencoded string. """ request.realms = self.request_validator.get_realms( @@ -120,7 +121,8 @@ def create_access_token_response(self, uri, http_method='GET', body=None, def validate_access_token_request(self, request): """Validate an access token request. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :raises: OAuth1Error if the request is invalid. :returns: A tuple of 2 elements. 1. The validation result (True or False). diff --git a/oauthlib/oauth1/rfc5849/endpoints/authorization.py b/oauthlib/oauth1/rfc5849/endpoints/authorization.py index 1751a45e..b465946f 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/authorization.py +++ b/oauthlib/oauth1/rfc5849/endpoints/authorization.py @@ -42,7 +42,8 @@ class AuthorizationEndpoint(BaseEndpoint): def create_verifier(self, request, credentials): """Create and save a new request token. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :param credentials: A dict of extra token credentials. :returns: The verifier as a dict. """ diff --git a/oauthlib/oauth1/rfc5849/endpoints/request_token.py b/oauthlib/oauth1/rfc5849/endpoints/request_token.py index 88fd6c09..e9ca331d 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/request_token.py +++ b/oauthlib/oauth1/rfc5849/endpoints/request_token.py @@ -34,7 +34,8 @@ class RequestTokenEndpoint(BaseEndpoint): def create_request_token(self, request, credentials): """Create and save a new request token. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :param credentials: A dict of extra token credentials. :returns: The token as an urlencoded string. """ @@ -111,7 +112,8 @@ def create_request_token_response(self, uri, http_method='GET', body=None, def validate_request_token_request(self, request): """Validate a request token request. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :raises: OAuth1Error if the request is invalid. :returns: A tuple of 2 elements. 1. The validation result (True or False). diff --git a/oauthlib/oauth1/rfc5849/request_validator.py b/oauthlib/oauth1/rfc5849/request_validator.py index bc62ea04..330bcbb8 100644 --- a/oauthlib/oauth1/rfc5849/request_validator.py +++ b/oauthlib/oauth1/rfc5849/request_validator.py @@ -267,7 +267,8 @@ def get_client_secret(self, client_key, request): """Retrieves the client secret associated with the client key. :param client_key: The client/consumer key. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: The client secret as a string. This method must allow the use of a dummy client_key value. @@ -303,7 +304,8 @@ def get_request_token_secret(self, client_key, token, request): :param client_key: The client/consumer key. :param token: The request token string. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: The token secret as a string. This method must allow the use of a dummy values and the running time @@ -335,7 +337,8 @@ def get_access_token_secret(self, client_key, token, request): :param client_key: The client/consumer key. :param token: The access token string. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: The token secret as a string. This method must allow the use of a dummy values and the running time @@ -366,7 +369,8 @@ def get_default_realms(self, client_key, request): """Get the default realms for a client. :param client_key: The client/consumer key. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: The list of default realms associated with the client. The list of default realms will be set during client registration and @@ -382,7 +386,8 @@ def get_realms(self, token, request): """Get realms associated with a request token. :param token: The request token string. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: The list of realms associated with the request token. This method is used by @@ -396,7 +401,8 @@ def get_redirect_uri(self, token, request): """Get the redirect URI associated with a request token. :param token: The request token string. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: The redirect URI associated with the request token. It may be desirable to return a custom URI if the redirect is set to "oob". @@ -413,7 +419,8 @@ def get_rsa_key(self, client_key, request): """Retrieves a previously stored client provided RSA key. :param client_key: The client/consumer key. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: The rsa public key as a string. This method must allow the use of a dummy client_key value. Fetching @@ -437,7 +444,8 @@ def invalidate_request_token(self, client_key, request_token, request): :param client_key: The client/consumer key. :param request_token: The request token string. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: None Per `Section 2.3`__ of the spec: @@ -462,7 +470,8 @@ def validate_client_key(self, client_key, request): """Validates that supplied client key is a registered and valid client. :param client_key: The client/consumer key. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: True or False Note that if the dummy client is supplied it should validate in same @@ -499,7 +508,8 @@ def validate_request_token(self, client_key, token, request): :param client_key: The client/consumer key. :param token: The request token string. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: True or False Note that if the dummy request_token is supplied it should validate in @@ -533,7 +543,8 @@ def validate_access_token(self, client_key, token, request): :param client_key: The client/consumer key. :param token: The access token string. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: True or False Note that if the dummy access token is supplied it should validate in @@ -571,7 +582,8 @@ def validate_timestamp_and_nonce(self, client_key, timestamp, nonce, :param nonce: The ``oauth_nonce`` parameter. :param request_token: Request token string, if any. :param access_token: Access token string, if any. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: True or False Per `Section 3.3`_ of the spec. @@ -618,7 +630,8 @@ def validate_redirect_uri(self, client_key, redirect_uri, request): :param client_key: The client/consumer key. :param redirect_uri: The URI the client which to redirect back to after authorization is successful. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: True or False It is highly recommended that OAuth providers require their clients @@ -650,7 +663,8 @@ def validate_requested_realms(self, client_key, realms, request): :param client_key: The client/consumer key. :param realms: The list of realms that client is requesting access to. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: True or False This method is invoked when obtaining a request token and should @@ -669,7 +683,8 @@ def validate_realms(self, client_key, token, request, uri=None, :param client_key: The client/consumer key. :param token: A request token string. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :param uri: The URI the realms is protecting. :param realms: A list of realms that must have been granted to the access token. @@ -703,7 +718,8 @@ def validate_verifier(self, client_key, token, verifier, request): :param client_key: The client/consumer key. :param token: A request token string. :param verifier: The authorization verifier string. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: True or False OAuth providers issue a verification code to clients after the @@ -732,7 +748,8 @@ def verify_request_token(self, token, request): """Verify that the given OAuth1 request token is valid. :param token: A request token string. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: True or False This method is used only in AuthorizationEndpoint to check whether the @@ -751,7 +768,8 @@ def verify_realms(self, token, realms, request): :param token: An access token string. :param realms: A list of realms the client attempts to access. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :returns: True or False This prevents the list of authorized realms sent by the client during @@ -773,7 +791,8 @@ def save_access_token(self, token, request): """Save an OAuth1 access token. :param token: A dict with token credentials. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request The token dictionary will at minimum include @@ -796,7 +815,8 @@ def save_request_token(self, token, request): """Save an OAuth1 request token. :param token: A dict with token credentials. - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request The token dictionary will at minimum include @@ -818,7 +838,8 @@ def save_verifier(self, token, verifier, request): :param token: A request token string. :param verifier A dictionary containing the oauth_verifier and oauth_token - :param request: An oauthlib.common.Request object. + :param request: OAuthlib request. + :type request: oauthlib.common.Request We need to associate verifiers with tokens for validation during the access token request. diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index ab4c1840..59366b16 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -98,7 +98,12 @@ class AuthorizationCodeGrant(GrantTypeBase): response_types = ['code'] def create_authorization_code(self, request): - """Generates an authorization grant represented as a dictionary.""" + """ + Generates an authorization grant represented as a dictionary. + + :param request: OAuthlib request. + :type request: oauthlib.common.Request + """ grant = {'code': common.generate_token()} if hasattr(request, 'state') and request.state: grant['state'] = request.state @@ -135,7 +140,8 @@ def create_authorization_response(self, request, token_handler): HTTP redirection response, or by other means available to it via the user-agent. - :param request: oauthlib.commong.Request + :param request: OAuthlib request. + :type request: oauthlib.common.Request :param token_handler: A token handler instace, for example of type oauthlib.oauth2.BearerToken. :returns: headers, body, status @@ -220,6 +226,12 @@ def create_token_response(self, request, token_handler): MUST deny the request and SHOULD revoke (when possible) all tokens previously issued based on that authorization code. The authorization code is bound to the client identifier and redirection URI. + + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param token_handler: A token handler instace, for example of type + oauthlib.oauth2.BearerToken. + """ headers = { 'Content-Type': 'application/json', @@ -253,6 +265,9 @@ def validate_authorization_request(self, request): missing. These must be caught by the provider and handled, how this is done is outside of the scope of OAuthLib but showing an error page describing the issue is a good idea. + + :param request: OAuthlib request. + :type request: oauthlib.common.Request """ # First check for fatal errors @@ -353,6 +368,10 @@ def validate_authorization_request(self, request): return request.scopes, request_info def validate_token_request(self, request): + """ + :param request: OAuthlib request. + :type request: oauthlib.common.Request + """ # REQUIRED. Value MUST be set to "authorization_code". if request.grant_type not in ('authorization_code', 'openid'): raise errors.UnsupportedGrantTypeError(request=request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/base.py b/oauthlib/oauth2/rfc6749/grant_types/base.py index e5d8ddd1..4d9381c4 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/base.py +++ b/oauthlib/oauth2/rfc6749/grant_types/base.py @@ -116,14 +116,32 @@ def register_code_modifier(self, modifier): def register_token_modifier(self, modifier): self._token_modifiers.append(modifier) - def create_authorization_response(self, request, token_handler): + """ + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param token_handler: A token handler instace, for example of type + oauthlib.oauth2.BearerToken. + """ raise NotImplementedError('Subclasses must implement this method.') def create_token_response(self, request, token_handler): + """ + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param token_handler: A token handler instace, for example of type + oauthlib.oauth2.BearerToken. + """ raise NotImplementedError('Subclasses must implement this method.') def add_token(self, token, token_handler, request): + """ + :param token: + :param token_handler: A token handler instace, for example of type + oauthlib.oauth2.BearerToken. + :param request: OAuthlib request. + :type request: oauthlib.common.Request + """ # Only add a hybrid access token on auth step if asked for if not request.response_type in ["token", "code token", "id_token token", "code id_token token"]: return token @@ -132,6 +150,10 @@ def add_token(self, token, token_handler, request): return token def validate_grant_type(self, request): + """ + :param request: OAuthlib request. + :type request: oauthlib.common.Request + """ client_id = getattr(request, 'client_id', None) if not self.request_validator.validate_grant_type(client_id, request.grant_type, request.client, request): @@ -140,6 +162,10 @@ def validate_grant_type(self, request): raise errors.UnauthorizedClientError(request=request) def validate_scopes(self, request): + """ + :param request: OAuthlib request. + :type request: oauthlib.common.Request + """ if not request.scopes: request.scopes = utils.scope_to_list(request.scope) or utils.scope_to_list( self.request_validator.get_default_scopes(request.client_id, request)) @@ -154,6 +180,13 @@ def prepare_authorization_response(self, request, token, headers, body, status): Base classes can define a default response mode for their authorization response by overriding the static `default_response_mode` member. + + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param token: + :param headers: + :param body: + :param status: """ request.response_mode = request.response_mode or self.default_response_mode diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py index 4c50a78a..884363f6 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py @@ -53,6 +53,11 @@ class ClientCredentialsGrant(GrantTypeBase): def create_token_response(self, request, token_handler): """Return token or error in JSON format. + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param token_handler: A token handler instace, for example of type + oauthlib.oauth2.BearerToken. + If the access token request is valid and authorized, the authorization server issues an access token as described in `Section 5.1`_. A refresh token SHOULD NOT be included. If the request @@ -85,6 +90,10 @@ def create_token_response(self, request, token_handler): return headers, json.dumps(token), 200 def validate_token_request(self, request): + """ + :param request: OAuthlib request. + :type request: oauthlib.common.Request + """ for validator in self.custom_validators.pre_token: validator(request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py index 3a5c0582..600c0a57 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py +++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py @@ -121,6 +121,12 @@ class ImplicitGrant(GrantTypeBase): def create_authorization_response(self, request, token_handler): """Create an authorization response. + + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param token_handler: A token handler instace, for example of type + oauthlib.oauth2.BearerToken. + The client constructs the request URI by adding the following parameters to the query component of the authorization endpoint URI using the "application/x-www-form-urlencoded" format, per `Appendix B`_: @@ -163,6 +169,11 @@ def create_authorization_response(self, request, token_handler): def create_token_response(self, request, token_handler): """Return token or error embedded in the URI fragment. + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param token_handler: A token handler instace, for example of type + oauthlib.oauth2.BearerToken. + If the resource owner grants the access request, the authorization server issues an access token and delivers it to the client by adding the following parameters to the fragment component of the redirection @@ -243,11 +254,18 @@ def create_token_response(self, request, token_handler): request, token, {}, None, 302) def validate_authorization_request(self, request): + """ + :param request: OAuthlib request. + :type request: oauthlib.common.Request + """ return self.validate_token_request(request) def validate_token_request(self, request): """Check the token request for normal and fatal errors. + :param request: OAuthlib request. + :type request: oauthlib.common.Request + This method is very similar to validate_authorization_request in the AuthorizationCodeGrant but differ in a few subtle areas. diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py index c2d86f79..55ddbb2c 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -33,6 +33,11 @@ def __init__(self, request_validator=None, def create_token_response(self, request, token_handler): """Create a new access token from a refresh_token. + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param token_handler: A token handler instace, for example of type + oauthlib.oauth2.BearerToken. + If valid and authorized, the authorization server issues an access token as described in `Section 5.1`_. If the request failed verification or is invalid, the authorization server returns an error @@ -72,6 +77,10 @@ def create_token_response(self, request, token_handler): return headers, json.dumps(token), 200 def validate_token_request(self, request): + """ + :param request: OAuthlib request. + :type request: oauthlib.common.Request + """ # REQUIRED. Value MUST be set to "refresh_token". if request.grant_type != 'refresh_token': raise errors.UnsupportedGrantTypeError(request=request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py index e5f04af4..25fb1f19 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py @@ -73,6 +73,11 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase): def create_token_response(self, request, token_handler): """Return token or error in json format. + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param token_handler: A token handler instace, for example of type + oauthlib.oauth2.BearerToken. + If the access token request is valid and authorized, the authorization server issues an access token and optional refresh token as described in `Section 5.1`_. If the request failed client @@ -114,6 +119,9 @@ def create_token_response(self, request, token_handler): def validate_token_request(self, request): """ + :param request: OAuthlib request. + :type request: oauthlib.common.Request + The client makes a request to the token endpoint by adding the following parameters using the "application/x-www-form-urlencoded" format per Appendix B with a character encoding of UTF-8 in the HTTP diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index c5127e7e..3f18733a 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -37,14 +37,13 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None, using the ``application/x-www-form-urlencoded`` format as defined by [`W3C.REC-html401-19991224`_]: + :param client_id: The client identifier as described in `Section 2.2`_. :param response_type: To indicate which OAuth 2 grant/flow is required, "code" and "token". - :param client_id: The client identifier as described in `Section 2.2`_. :param redirect_uri: The client provided URI to redirect back to after authorization as described in `Section 3.1.2`_. :param scope: The scope of the access request as described by `Section 3.3`_. - :param state: An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent @@ -133,15 +132,19 @@ def prepare_token_revocation_request(url, token, token_type_hint="access_token", using the "application/x-www-form-urlencoded" format in the HTTP request entity-body: - token REQUIRED. The token that the client wants to get revoked. - - token_type_hint OPTIONAL. A hint about the type of the token submitted - for revocation. Clients MAY pass this parameter in order to help the - authorization server to optimize the token lookup. If the server is unable - to locate the token using the given hint, it MUST extend its search across - all of its supported token types. An authorization server MAY ignore this - parameter, particularly if it is able to detect the token type - automatically. This specification defines two such values: + :param token: REQUIRED. The token that the client wants to get revoked. + + param:token_type_hint: OPTIONAL. A hint about the type of the token + submitted for revocation. Clients MAY pass this + parameter in order to help the authorization server + to optimize the token lookup. If the server is + unable to locate the token using the given hint, it + MUST extend its search across all of its supported + token types. An authorization server MAY ignore this + parameter, particularly if it is able to detect the + token type automatically. + + This specification defines two values for `token_type_hint`: * access_token: An access token as defined in [RFC6749], `Section 1.4`_ diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index ff3bbd64..6ce7910b 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -26,7 +26,8 @@ def client_authentication_required(self, request, *args, **kwargs): client credentials or whenever Client provided client authentication, see `Section 6`_ - :param request: oauthlib.common.Request + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -51,7 +52,8 @@ def authenticate_client(self, request, *args, **kwargs): both body and query can be obtained by direct attribute access, i.e. request.client_id for client_id in the URL query. - :param request: oauthlib.common.Request + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -74,7 +76,9 @@ def authenticate_client_id(self, client_id, request, *args, **kwargs): to set request.client to the client object associated with the given client_id. - :param request: oauthlib.common.Request + :param client_id: Unicode client identifier. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -93,11 +97,12 @@ def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request, the client's allowed redirect URIs, but against the URI used when the code was saved. - :param client_id: Unicode client identifier + :param client_id: Unicode client identifier. :param code: Unicode authorization_code. - :param redirect_uri: Unicode absolute URI + :param redirect_uri: Unicode absolute URI. :param client: Client object set by you, see authenticate_client. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -108,8 +113,9 @@ def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request, def get_default_redirect_uri(self, client_id, request, *args, **kwargs): """Get the default redirect URI for the client. - :param client_id: Unicode client identifier - :param request: The HTTP Request (oauthlib.common.Request) + :param client_id: Unicode client identifier. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: The default redirect URI for the client Method is used by: @@ -121,8 +127,9 @@ def get_default_redirect_uri(self, client_id, request, *args, **kwargs): def get_default_scopes(self, client_id, request, *args, **kwargs): """Get the default scopes for the client. - :param client_id: Unicode client identifier - :param request: The HTTP Request (oauthlib.common.Request) + :param client_id: Unicode client identifier. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: List of default scopes Method is used by all core grant types: @@ -136,8 +143,9 @@ def get_default_scopes(self, client_id, request, *args, **kwargs): def get_original_scopes(self, refresh_token, request, *args, **kwargs): """Get the list of scopes associated with the refresh token. - :param refresh_token: Unicode refresh token - :param request: The HTTP Request (oauthlib.common.Request) + :param refresh_token: Unicode refresh token. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: List of scopes. Method is used by: @@ -156,9 +164,10 @@ def is_within_original_scope(self, request_scopes, refresh_token, request, *args used in situations where returning all valid scopes from the get_original_scopes is not practical. - :param request_scopes: A list of scopes that were requested by client - :param refresh_token: Unicode refresh_token - :param request: The HTTP Request (oauthlib.common.Request) + :param request_scopes: A list of scopes that were requested by client. + :param refresh_token: Unicode refresh_token. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -196,7 +205,8 @@ def introspect_token(self, token, token_type_hint, request, *args, **kwargs): :param token: The token string. :param token_type_hint: access_token or refresh_token. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request Method is used by: - Introspect Endpoint (all grants are compatible) @@ -209,9 +219,10 @@ def introspect_token(self, token, token_type_hint, request, *args, **kwargs): def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs): """Invalidate an authorization code after use. - :param client_id: Unicode client identifier + :param client_id: Unicode client identifier. :param code: The authorization code grant (request.code). - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request Method is used by: - Authorization Code Grant @@ -223,7 +234,8 @@ def revoke_token(self, token, token_type_hint, request, *args, **kwargs): :param token: The token string. :param token_type_hint: access_token or refresh_token. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request Method is used by: - Revocation Endpoint @@ -237,7 +249,8 @@ def rotate_refresh_token(self, request): or replaced with a new one (rotated). Return True to rotate and and False for keeping original. - :param request: oauthlib.common.Request + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -269,9 +282,10 @@ def save_authorization_code(self, client_id, code, request, *args, **kwargs): http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter This value should be saved in this method and used again in 'validate_code'. - :param client_id: Unicode client identifier + :param client_id: Unicode client identifier. :param code: A dict of the authorization code grant and, optionally, state. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request Method is used by: - Authorization Code Grant @@ -292,10 +306,12 @@ def get_authorization_code_scopes(self, client_id, code, redirect_uri, request): blank value `""` don't forget to check it before using those values in a select query if a database is used. - :param client_id: Unicode client identifier - :param code: Unicode authorization code grant - :param redirect_uri: Unicode absolute URI - :return: A list of scope + :param client_id: Unicode client identifier. + :param code: Unicode authorization code grant. + :param redirect_uri: Unicode absolute URI. + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :return: A list of scopes Method is used by: - Authorization Token Grant Dispatcher @@ -306,6 +322,10 @@ def save_token(self, token, request, *args, **kwargs): """Persist the token with a token type specific method. Currently, only save_bearer_token is supported. + + :param token: A (Bearer) token dict. + :param request: OAuthlib request. + :type request: oauthlib.common.Request """ return self.save_bearer_token(token, request, *args, **kwargs) @@ -346,8 +366,9 @@ def save_bearer_token(self, token, request, *args, **kwargs): the claims dict, which should be saved for later use when generating the id_token and/or UserInfo response content. - :param token: A Bearer token dict - :param request: The HTTP Request (oauthlib.common.Request) + :param token: A Bearer token dict. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: The default redirect URI for the client Method is used by all core grant types issuing Bearer tokens: @@ -363,9 +384,10 @@ def get_jwt_bearer_token(self, token, token_handler, request): If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token` - :param token: A Bearer token dict - :param token_handler: the token handler (BearerToken class) - :param request: the HTTP Request (oauthlib.common.Request) + :param token: A Bearer token dict. + :param token_handler: The token handler (BearerToken class). + :param request: OAuthlib request. + :type request: oauthlib.common.Request :return: The JWT Bearer token or OpenID Connect ID token (a JWS signed JWT) Method is used by JWT Bearer and OpenID Connect tokens: @@ -398,9 +420,10 @@ def get_id_token(self, token, token_handler, request): .. _`3.2.2.10`: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken .. _`3.3.2.11`: http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken - :param token: A Bearer token dict - :param token_handler: the token handler (BearerToken class) - :param request: the HTTP Request (oauthlib.common.Request) + :param token: A Bearer token dict. + :param token_handler: The token handler (BearerToken class) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :return: The ID Token (a JWS signed JWT) """ # the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token @@ -419,9 +442,10 @@ def validate_jwt_bearer_token(self, token, scopes, request): - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2 - :param token: Unicode Bearer token - :param scopes: List of scopes (defined by you) - :param request: The HTTP Request (oauthlib.common.Request) + :param token: Unicode Bearer token. + :param scopes: List of scopes (defined by you). + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is indirectly used by all core OpenID connect JWT token issuing grant types: @@ -440,9 +464,10 @@ def validate_id_token(self, token, scopes, request): - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2 - :param token: Unicode Bearer token - :param scopes: List of scopes (defined by you) - :param request: The HTTP Request (oauthlib.common.Request) + :param token: Unicode Bearer token. + :param scopes: List of scopes (defined by you). + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is indirectly used by all core OpenID connect JWT token issuing grant types: @@ -457,7 +482,8 @@ def validate_bearer_token(self, token, scopes, request): :param token: A string of random characters. :param scopes: A list of scopes associated with the protected resource. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request A key to OAuth 2 security and restricting impact of leaked tokens is the short expiration time of tokens, *always ensure the token has not @@ -491,7 +517,8 @@ def validate_bearer_token(self, token, scopes, request): :param token: Unicode Bearer token :param scopes: List of scopes (defined by you) - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is indirectly used by all core Bearer token issuing grant types: @@ -509,7 +536,9 @@ def validate_client_id(self, client_id, request, *args, **kwargs): to set request.client to the client object associated with the given client_id. - :param request: oauthlib.common.Request + :param client_id: Unicode client identifier. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -535,10 +564,11 @@ def validate_code(self, client_id, code, client, request, *args, **kwargs): The request.claims property, if it was given, should assigned a dict. - :param client_id: Unicode client identifier - :param code: Unicode authorization code + :param client_id: Unicode client identifier. + :param code: Unicode authorization code. :param client: Client object set by you, see authenticate_client. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -549,10 +579,11 @@ def validate_code(self, client_id, code, client, request, *args, **kwargs): def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs): """Ensure client is authorized to use the grant_type requested. - :param client_id: Unicode client identifier + :param client_id: Unicode client identifier. :param grant_type: Unicode grant type, i.e. authorization_code, password. :param client: Client object set by you, see authenticate_client. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -569,9 +600,10 @@ def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwarg All clients should register the absolute URIs of all URIs they intend to redirect to. The registration is outside of the scope of oauthlib. - :param client_id: Unicode client identifier - :param redirect_uri: Unicode absolute URI - :param request: The HTTP Request (oauthlib.common.Request) + :param client_id: Unicode client identifier. + :param redirect_uri: Unicode absolute URI. + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -586,9 +618,10 @@ def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs OBS! The request.user attribute should be set to the resource owner associated with this refresh token. - :param refresh_token: Unicode refresh token + :param refresh_token: Unicode refresh token. :param client: Client object set by you, see authenticate_client. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -601,10 +634,11 @@ def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs): """Ensure client is authorized to use the response_type requested. - :param client_id: Unicode client identifier + :param client_id: Unicode client identifier. :param response_type: Unicode response type, i.e. code, token. :param client: Client object set by you, see authenticate_client. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -616,10 +650,11 @@ def validate_response_type(self, client_id, response_type, client, request, *arg def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): """Ensure the client is authorized access to requested scopes. - :param client_id: Unicode client identifier - :param scopes: List of scopes (defined by you) + :param client_id: Unicode client identifier. + :param scopes: List of scopes (defined by you). :param client: Client object set by you, see authenticate_client. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by all core grant types: @@ -636,7 +671,8 @@ def validate_silent_authorization(self, request): Silent OpenID authorization allows access tokens and id tokens to be granted to clients without any user prompt or interaction. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -656,7 +692,8 @@ def validate_silent_login(self, request): not selected which one to link to the token then this method should raise an oauthlib.oauth2.AccountSelectionRequired error. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -674,10 +711,11 @@ def validate_user(self, username, password, client, request, *args, **kwargs): not set you will be unable to associate a token with a user in the persistance method used (commonly, save_bearer_token). - :param username: Unicode username - :param password: Unicode password + :param username: Unicode username. + :param password: Unicode password. :param client: Client object set by you, see authenticate_client. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -694,7 +732,8 @@ def validate_user_match(self, id_token_hint, scopes, claims, request): :param id_token_hint: User identifier string. :param scopes: List of OAuth 2 scopes and OpenID claims (strings). :param claims: OpenID Connect claims dict. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index 1d2b5eb4..765251e5 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -96,10 +96,14 @@ def prepare_mac_header(token, uri, key, http_method, .. _`MAC Access Authentication`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01 .. _`extension algorithms`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1 + :param token: :param uri: Request URI. - :param headers: Request headers as a dictionary. - :param http_method: HTTP Request method. :param key: MAC given provided by token endpoint. + :param http_method: HTTP Request method. + :param nonce: + :param headers: Request headers as a dictionary. + :param body: + :param ext: :param hash_algorithm: HMAC algorithm provided by token endpoint. :param issue_time: Time when the MAC credentials were issued (datetime). :param draft: MAC authentication specification version. @@ -181,6 +185,9 @@ def prepare_bearer_uri(token, uri): http://www.example.com/path?access_token=h480djs93hd8 .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750 + + :param token: + :param uri: """ return add_params_to_uri(uri, [(('access_token', token))]) @@ -192,6 +199,9 @@ def prepare_bearer_headers(token, headers=None): Authorization: Bearer h480djs93hd8 .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750 + + :param token: + :param headers: """ headers = headers or {} headers['Authorization'] = 'Bearer %s' % token @@ -204,15 +214,26 @@ def prepare_bearer_body(token, body=''): access_token=h480djs93hd8 .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750 + + :param token: + :param body: """ return add_params_to_qs(body, [(('access_token', token))]) def random_token_generator(request, refresh_token=False): + """ + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param refresh_token: + """ return common.generate_token() def signed_token_generator(private_pem, **kwargs): + """ + :param private_pem: + """ def signed_token_generator(request): request.claims = kwargs return common.generate_signed_token(private_pem, request) @@ -223,7 +244,8 @@ def signed_token_generator(request): def get_token_from_header(request): """ Helper function to extract a token from the request header. - :param request: The request object + :param request: OAuthlib request. + :type request: oauthlib.common.Request :return: Return the token or None if the Authorization header is malformed. """ token = None @@ -244,9 +266,17 @@ def __call__(self, request, refresh_token=False): raise NotImplementedError('Subclasses must implement this method.') def validate_request(self, request): + """ + :param request: OAuthlib request. + :type request: oauthlib.common.Request + """ raise NotImplementedError('Subclasses must implement this method.') def estimate_type(self, request): + """ + :param request: OAuthlib request. + :type request: oauthlib.common.Request + """ raise NotImplementedError('Subclasses must implement this method.') @@ -266,7 +296,14 @@ def __init__(self, request_validator=None, token_generator=None, self.expires_in = expires_in or 3600 def create_token(self, request, refresh_token=False, save_token=True): - """Create a BearerToken, by default without refresh token.""" + """ + Create a BearerToken, by default without refresh token. + + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param refresh_token: + :param save_token: + """ if callable(self.expires_in): expires_in = self.expires_in(request) @@ -304,11 +341,19 @@ def create_token(self, request, refresh_token=False, save_token=True): return token def validate_request(self, request): + """ + :param request: OAuthlib request. + :type request: oauthlib.common.Request + """ token = get_token_from_header(request) return self.request_validator.validate_bearer_token( token, request.scopes, request) def estimate_type(self, request): + """ + :param request: OAuthlib request. + :type request: oauthlib.common.Request + """ if request.headers.get('Authorization', '').split(' ')[0] == 'Bearer': return 9 elif request.access_token is not None: diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py index f3bcbdb5..15877541 100644 --- a/oauthlib/openid/connect/core/request_validator.py +++ b/oauthlib/openid/connect/core/request_validator.py @@ -45,7 +45,8 @@ def get_jwt_bearer_token(self, token, token_handler, request): :param token: A Bearer token dict :param token_handler: the token handler (BearerToken class) - :param request: the HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :return: The JWT Bearer token or OpenID Connect ID token (a JWS signed JWT) Method is used by JWT Bearer and OpenID Connect tokens: @@ -80,7 +81,8 @@ def get_id_token(self, token, token_handler, request): :param token: A Bearer token dict :param token_handler: the token handler (BearerToken class) - :param request: the HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :return: The ID Token (a JWS signed JWT) """ # the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token @@ -101,7 +103,8 @@ def validate_jwt_bearer_token(self, token, scopes, request): :param token: Unicode Bearer token :param scopes: List of scopes (defined by you) - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is indirectly used by all core OpenID connect JWT token issuing grant types: @@ -122,7 +125,8 @@ def validate_id_token(self, token, scopes, request): :param token: Unicode Bearer token :param scopes: List of scopes (defined by you) - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is indirectly used by all core OpenID connect JWT token issuing grant types: @@ -138,7 +142,8 @@ def validate_silent_authorization(self, request): Silent OpenID authorization allows access tokens and id tokens to be granted to clients without any user prompt or interaction. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -158,7 +163,8 @@ def validate_silent_login(self, request): not selected which one to link to the token then this method should raise an oauthlib.oauth2.AccountSelectionRequired error. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: @@ -177,7 +183,8 @@ def validate_user_match(self, id_token_hint, scopes, claims, request): :param id_token_hint: User identifier string. :param scopes: List of OAuth 2 scopes and OpenID claims (strings). :param claims: OpenID Connect claims dict. - :param request: The HTTP Request (oauthlib.common.Request) + :param request: OAuthlib request. + :type request: oauthlib.common.Request :rtype: True or False Method is used by: From b02809db83ad490f6ff3f1b86037272bebb4e041 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 11 Sep 2018 09:09:50 +0200 Subject: [PATCH 087/237] Try to improve multibuild coverage --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index dd72d5cc..f46bf439 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,10 +17,11 @@ install: - pip install -U setuptools - pip install tox coveralls script: tox -after_success: coveralls +after_success: COVERALLS_PARALLEL=true coveralls notifications: webhooks: urls: + - https://coveralls.io/webhook - https://webhooks.gitter.im/e/6008c872bf0ecee344f4 on_success: change on_failure: always From 0e963d8e55cabbd9b50fe2d6ec6659f01c1a2e00 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Tue, 11 Sep 2018 14:34:21 -0400 Subject: [PATCH 088/237] cleanup on docs fixes --- .../rfc6749/clients/service_application.py | 2 +- oauthlib/oauth2/rfc6749/errors.py | 37 ++++++++++--------- .../rfc6749/grant_types/authorization_code.py | 4 +- oauthlib/oauth2/rfc6749/grant_types/base.py | 6 +-- .../rfc6749/grant_types/client_credentials.py | 2 +- .../oauth2/rfc6749/grant_types/implicit.py | 4 +- .../rfc6749/grant_types/refresh_token.py | 2 +- .../resource_owner_password_credentials.py | 2 +- oauthlib/oauth2/rfc6749/parameters.py | 23 +++++++----- oauthlib/oauth2/rfc6749/request_validator.py | 14 +++---- oauthlib/oauth2/rfc6749/tokens.py | 1 + 11 files changed, 53 insertions(+), 44 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index 7f336bbc..3045676b 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -54,7 +54,7 @@ def __init__(self, client_id, private_key=None, subject=None, issuer=None, ``https://provider.com/oauth2/token``. :param kwargs: Additional arguments to pass to base client, such as - state and token. See Client.__init__.__doc__ for + state and token. See ``Client.__init__.__doc__`` for details. """ super(ServiceApplicationClient, self).__init__(client_id, **kwargs) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 5a0cca2b..3f0bfc0e 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -21,23 +21,26 @@ class OAuth2Error(Exception): def __init__(self, description=None, uri=None, state=None, status_code=None, request=None): """ - description: A human-readable ASCII [USASCII] text providing - additional information, used to assist the client - developer in understanding the error that occurred. - Values for the "error_description" parameter MUST NOT - include characters outside the set - x20-21 / x23-5B / x5D-7E. - - uri: A URI identifying a human-readable web page with information - about the error, used to provide the client developer with - additional information about the error. Values for the - "error_uri" parameter MUST conform to the URI- Reference - syntax, and thus MUST NOT include characters outside the set - x21 / x23-5B / x5D-7E. - - state: A CSRF protection value received from the client. - - request: Oauthlib Request object + :param description: A human-readable ASCII [USASCII] text providing + additional information, used to assist the client + developer in understanding the error that occurred. + Values for the "error_description" parameter + MUST NOT include characters outside the set + x20-21 / x23-5B / x5D-7E. + + :param uri: A URI identifying a human-readable web page with information + about the error, used to provide the client developer with + additional information about the error. Values for the + "error_uri" parameter MUST conform to the URI- Reference + syntax, and thus MUST NOT include characters outside the set + x21 / x23-5B / x5D-7E. + + :param state: A CSRF protection value received from the client. + + :param status_code: + + :param request: OAuthlib request. + :type request: oauthlib.common.Request """ if description is not None: self.description = description diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 59366b16..8ebae498 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -142,7 +142,7 @@ def create_authorization_response(self, request, token_handler): :param request: OAuthlib request. :type request: oauthlib.common.Request - :param token_handler: A token handler instace, for example of type + :param token_handler: A token handler instance, for example of type oauthlib.oauth2.BearerToken. :returns: headers, body, status :raises: FatalClientError on invalid redirect URI or client id. @@ -229,7 +229,7 @@ def create_token_response(self, request, token_handler): :param request: OAuthlib request. :type request: oauthlib.common.Request - :param token_handler: A token handler instace, for example of type + :param token_handler: A token handler instance, for example of type oauthlib.oauth2.BearerToken. """ diff --git a/oauthlib/oauth2/rfc6749/grant_types/base.py b/oauthlib/oauth2/rfc6749/grant_types/base.py index 4d9381c4..43f9db40 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/base.py +++ b/oauthlib/oauth2/rfc6749/grant_types/base.py @@ -120,7 +120,7 @@ def create_authorization_response(self, request, token_handler): """ :param request: OAuthlib request. :type request: oauthlib.common.Request - :param token_handler: A token handler instace, for example of type + :param token_handler: A token handler instance, for example of type oauthlib.oauth2.BearerToken. """ raise NotImplementedError('Subclasses must implement this method.') @@ -129,7 +129,7 @@ def create_token_response(self, request, token_handler): """ :param request: OAuthlib request. :type request: oauthlib.common.Request - :param token_handler: A token handler instace, for example of type + :param token_handler: A token handler instance, for example of type oauthlib.oauth2.BearerToken. """ raise NotImplementedError('Subclasses must implement this method.') @@ -137,7 +137,7 @@ def create_token_response(self, request, token_handler): def add_token(self, token, token_handler, request): """ :param token: - :param token_handler: A token handler instace, for example of type + :param token_handler: A token handler instance, for example of type oauthlib.oauth2.BearerToken. :param request: OAuthlib request. :type request: oauthlib.common.Request diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py index 884363f6..7d4f74cb 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py @@ -55,7 +55,7 @@ def create_token_response(self, request, token_handler): :param request: OAuthlib request. :type request: oauthlib.common.Request - :param token_handler: A token handler instace, for example of type + :param token_handler: A token handler instance, for example of type oauthlib.oauth2.BearerToken. If the access token request is valid and authorized, the diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py index 600c0a57..b29953bf 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py +++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py @@ -124,7 +124,7 @@ def create_authorization_response(self, request, token_handler): :param request: OAuthlib request. :type request: oauthlib.common.Request - :param token_handler: A token handler instace, for example of type + :param token_handler: A token handler instance, for example of type oauthlib.oauth2.BearerToken. The client constructs the request URI by adding the following @@ -171,7 +171,7 @@ def create_token_response(self, request, token_handler): :param request: OAuthlib request. :type request: oauthlib.common.Request - :param token_handler: A token handler instace, for example of type + :param token_handler: A token handler instance, for example of type oauthlib.oauth2.BearerToken. If the resource owner grants the access request, the authorization diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py index 55ddbb2c..5f7382a7 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -35,7 +35,7 @@ def create_token_response(self, request, token_handler): :param request: OAuthlib request. :type request: oauthlib.common.Request - :param token_handler: A token handler instace, for example of type + :param token_handler: A token handler instance, for example of type oauthlib.oauth2.BearerToken. If valid and authorized, the authorization server issues an access diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py index 25fb1f19..87e80152 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py @@ -75,7 +75,7 @@ def create_token_response(self, request, token_handler): :param request: OAuthlib request. :type request: oauthlib.common.Request - :param token_handler: A token handler instace, for example of type + :param token_handler: A token handler instance, for example of type oauthlib.oauth2.BearerToken. If the access token request is valid and authorized, the diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 3f18733a..e1780f8f 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -37,6 +37,7 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None, using the ``application/x-www-form-urlencoded`` format as defined by [`W3C.REC-html401-19991224`_]: + :param uri: :param client_id: The client identifier as described in `Section 2.2`_. :param response_type: To indicate which OAuth 2 grant/flow is required, "code" and "token". @@ -134,15 +135,15 @@ def prepare_token_revocation_request(url, token, token_type_hint="access_token", :param token: REQUIRED. The token that the client wants to get revoked. - param:token_type_hint: OPTIONAL. A hint about the type of the token - submitted for revocation. Clients MAY pass this - parameter in order to help the authorization server - to optimize the token lookup. If the server is - unable to locate the token using the given hint, it - MUST extend its search across all of its supported - token types. An authorization server MAY ignore this - parameter, particularly if it is able to detect the - token type automatically. + :param token_type_hint: OPTIONAL. A hint about the type of the token + submitted for revocation. Clients MAY pass this + parameter in order to help the authorization server + to optimize the token lookup. If the server is + unable to locate the token using the given hint, it + MUST extend its search across all of its supported + token types. An authorization server MAY ignore + this parameter, particularly if it is able to detect + the token type automatically. This specification defines two values for `token_type_hint`: @@ -267,6 +268,10 @@ def parse_implicit_response(uri, state=None, scope=None): authorization request. The exact value received from the client. + :param uri: + :param state: + :param scope: + Similar to the authorization code response, but with a full token provided in the URL fragment: diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 6ce7910b..2cf1b82c 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -100,7 +100,7 @@ def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request, :param client_id: Unicode client identifier. :param code: Unicode authorization_code. :param redirect_uri: Unicode absolute URI. - :param client: Client object set by you, see authenticate_client. + :param client: Client object set by you, see ``.authenticate_client``. :param request: OAuthlib request. :type request: oauthlib.common.Request :rtype: True or False @@ -566,7 +566,7 @@ def validate_code(self, client_id, code, client, request, *args, **kwargs): :param client_id: Unicode client identifier. :param code: Unicode authorization code. - :param client: Client object set by you, see authenticate_client. + :param client: Client object set by you, see ``.authenticate_client``. :param request: OAuthlib request. :type request: oauthlib.common.Request :rtype: True or False @@ -581,7 +581,7 @@ def validate_grant_type(self, client_id, grant_type, client, request, *args, **k :param client_id: Unicode client identifier. :param grant_type: Unicode grant type, i.e. authorization_code, password. - :param client: Client object set by you, see authenticate_client. + :param client: Client object set by you, see ``.authenticate_client``. :param request: OAuthlib request. :type request: oauthlib.common.Request :rtype: True or False @@ -619,7 +619,7 @@ def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs associated with this refresh token. :param refresh_token: Unicode refresh token. - :param client: Client object set by you, see authenticate_client. + :param client: Client object set by you, see ``.authenticate_client``. :param request: OAuthlib request. :type request: oauthlib.common.Request :rtype: True or False @@ -636,7 +636,7 @@ def validate_response_type(self, client_id, response_type, client, request, *arg :param client_id: Unicode client identifier. :param response_type: Unicode response type, i.e. code, token. - :param client: Client object set by you, see authenticate_client. + :param client: Client object set by you, see ``.authenticate_client``. :param request: OAuthlib request. :type request: oauthlib.common.Request :rtype: True or False @@ -652,7 +652,7 @@ def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): :param client_id: Unicode client identifier. :param scopes: List of scopes (defined by you). - :param client: Client object set by you, see authenticate_client. + :param client: Client object set by you, see ``.authenticate_client``. :param request: OAuthlib request. :type request: oauthlib.common.Request :rtype: True or False @@ -713,7 +713,7 @@ def validate_user(self, username, password, client, request, *args, **kwargs): :param username: Unicode username. :param password: Unicode password. - :param client: Client object set by you, see authenticate_client. + :param client: Client object set by you, see ``.authenticate_client``. :param request: OAuthlib request. :type request: oauthlib.common.Request :rtype: True or False diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index 765251e5..d78df09e 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -244,6 +244,7 @@ def signed_token_generator(request): def get_token_from_header(request): """ Helper function to extract a token from the request header. + :param request: OAuthlib request. :type request: oauthlib.common.Request :return: Return the token or None if the Authorization header is malformed. From 7b843b112b9b330268d99a3b1e65c8381a7ad945 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Tue, 11 Sep 2018 14:40:39 -0400 Subject: [PATCH 089/237] fixed spacing --- oauthlib/oauth2/rfc6749/parameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index e1780f8f..74a15f9a 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -95,7 +95,7 @@ def prepare_token_request(grant_type, body='', **kwargs): format in the HTTP request entity-body: :param grant_type: To indicate grant type being used, i.e. "password", - "authorization_code" or "client_credentials". + "authorization_code" or "client_credentials". :param body: Existing request body to embed parameters in. :param code: If using authorization code grant, pass the previously obtained authorization code as the ``code`` argument. From 867802b14427378dc58516b1b1cabc8a7dbc6d14 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Tue, 11 Sep 2018 16:30:21 -0400 Subject: [PATCH 090/237] idea for documentation in contributing.rst --- docs/contributing.rst | 52 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/contributing.rst b/docs/contributing.rst index cbdb5194..771262d9 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -238,6 +238,58 @@ Furthermore, the pixel shortage is over. We want to see: * `grid` instead of `g` * `my_function_that_does_things` instead of `mftdt` +Be sure to write documentation! +------------------------------- + +Documentation isn't just good, it's great - and necessary with large packages +like OAuthlib. Please make sure the next person who reads your function/method +can quickly understand what it does and how. Also, please ensure the parameters +passed to each function are properly documented as well. + +The project has these goals/requests for docstrings that are designed to make +the autogenerated documentation read more cleanly: + +#. Every parameter in the function should be listed in the docstring, and + should appear in the same order as they appear in the function itself. +#. If you are unsure of the best wording for a parameter description, leave it + blank, but still include the `:param foo:` line. This will make it easier for + maintainers to see and edit. +#. Use an existing standardized description of a parameter that appears + elsewhere in this project's documentation whenever possible. For example, + `request` is used as a parameter throughout the project with the description + "OAuthlib request." - there is no reason to describe it differently in your + function. Parameter descriptions should be a sentence that ends with a + period - even if it is just two words. +#. When possible, include a `type` declaration for the parameter. For example, + a "request" param is often accompanied with `:type request: oauthlib.common.Request`. + The type is expected to be an object type reference, and should never end + in a period. +#. If there is a textual docstring (recommended), use a single blank line to + separate the docstring and the params. +#. When you cite class functions, please use backticks. + +Consolidated example + + def foo(self, request, client, bar=None, key=None): + """ + This method checks the `key` against the `client`. The `request` is + passed to maintain context. + + Example MAC Authorization header, linebreaks added for clarity + + Authorization: MAC id="h480djs93hd8", + nonce="1336363200:dj83hs9s", + mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM=" + + .. _`MAC Access Authentication`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01 + + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :param client: Client object set by you, see ``.authenticate_client``. + :param bar: + :param key: MAC given provided by token endpoint. + """ + How pull requests are checked, tested, and done =============================================== From 1b4fa60945022a1d8739d5ed6a9a915230fd1e5b Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 11 Sep 2018 17:56:22 -0700 Subject: [PATCH 091/237] Remove workarounds for unsupported Python 2.6 Python 2.6 support was removed in 91152df142bdde134d84ed27963cda8e6b878416. Drop unittest2 dependency. All necessary testing features are included in the stdlib unittest. --- setup.py | 7 +++---- tests/unittest/__init__.py | 21 ++------------------- tox.ini | 4 ---- 3 files changed, 5 insertions(+), 27 deletions(-) diff --git a/setup.py b/setup.py index 1d69e0d9..17dcd7b3 100755 --- a/setup.py +++ b/setup.py @@ -18,10 +18,9 @@ def fread(fn): with open(join(dirname(__file__), fn), 'r') as f: return f.read() -if sys.version_info[0] == 3: - tests_require = ['nose', 'cryptography', 'pyjwt>=1.0.0', 'blinker'] -else: - tests_require = ['nose', 'unittest2', 'cryptography', 'mock>=2.0', 'pyjwt>=1.0.0', 'blinker'] +tests_require = ['nose', 'cryptography', 'pyjwt>=1.0.0', 'blinker'] +if sys.version_info[0] == 2: + tests_require.append('mock>=2.0') rsa_require = ['cryptography'] signedtoken_require = ['cryptography', 'pyjwt>=1.0.0'] signals_require = ['blinker'] diff --git a/tests/unittest/__init__.py b/tests/unittest/__init__.py index 35b239ad..6cb79a68 100644 --- a/tests/unittest/__init__.py +++ b/tests/unittest/__init__.py @@ -1,32 +1,15 @@ import collections import sys +from unittest import TestCase try: import urlparse except ImportError: import urllib.parse as urlparse -try: - # check the system path first - from unittest2 import * -except ImportError: - if sys.version_info >= (2, 7): - # unittest2 features are native in Python 2.7 - from unittest import * - else: - raise - -# Python 3.1 does not provide assertIsInstance -if sys.version_info[1] == 1: - TestCase.assertIsInstance = lambda self, obj, cls: self.assertTrue(isinstance(obj, cls)) # Somewhat consistent itemsequal between all python versions -if sys.version_info[1] == 3: +if sys.version_info[0] == 3: TestCase.assertItemsEqual = TestCase.assertCountEqual -elif sys.version_info[0] == 2 and sys.version_info[1] == 6: - pass -else: - TestCase.assertItemsEqual = lambda self, a, b: self.assertEqual( - collections.Counter(list(a)), collections.Counter(list(b))) # URL comparison where query param order is insignifcant diff --git a/tox.ini b/tox.ini index eac7a1e6..b736cf1f 100644 --- a/tox.ini +++ b/tox.ini @@ -6,10 +6,6 @@ deps= -rrequirements-test.txt commands=nosetests -s --with-coverage --cover-html --cover-html-dir={toxinidir}/htmlcov-{envname} --cover-erase --cover-package=oauthlib -w tests -[testenv:py27] -deps=unittest2 - {[testenv]deps} - # tox -e docs to mimick readthedocs build. # as of today, RTD is using python2.7 and doesn't run "setup.py install" [testenv:docs] From d6dfe4afc23086913f9b571d7a1b7ee58af5d809 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Thu, 13 Sep 2018 15:15:47 -0400 Subject: [PATCH 092/237] * addresing ticket #585 * `prepare_request_body` client_id is deprecated in favor of include_client_id * a new unit test `test_prepare_request_body` is added to ensure conformity of several use cases * the docstrings for the `body` param have been consolidated and standardized across multiple functions linked to `prepare_request_body` for clarity --- .../rfc6749/clients/backend_application.py | 2 + oauthlib/oauth2/rfc6749/clients/base.py | 6 ++- .../rfc6749/clients/legacy_application.py | 2 + .../rfc6749/clients/service_application.py | 13 ++--- .../oauth2/rfc6749/clients/web_application.py | 30 +++++++++--- oauthlib/oauth2/rfc6749/parameters.py | 4 +- .../rfc6749/clients/test_web_application.py | 49 ++++++++++++++++++- 7 files changed, 90 insertions(+), 16 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py index cbad8b74..99dbfc5d 100644 --- a/oauthlib/oauth2/rfc6749/clients/backend_application.py +++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py @@ -37,6 +37,8 @@ def prepare_request_body(self, body='', scope=None, **kwargs): following parameters using the "application/x-www-form-urlencoded" format per `Appendix B`_ in the HTTP request entity-body: + :param body: Existing request body (URL encoded string) to embed parameters + into. This may contain extra paramters. Default ''. :param scope: The scope of the access request as described by `Section 3.3`_. :param kwargs: Extra credentials to include in the token request. diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 406832d7..d8ded505 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -254,7 +254,8 @@ def prepare_token_request(self, token_url, authorization_response=None, :param redirect_url: The redirect_url supplied with the authorization request (if there was one). - :param body: Request body (URL encoded string). + :param body: Existing request body (URL encoded string) to embed parameters + into. This may contain extra paramters. Default ''. :param kwargs: Additional parameters to included in the request. @@ -286,7 +287,8 @@ def prepare_refresh_token_request(self, token_url, refresh_token=None, :param refresh_token: Refresh token string. - :param body: Request body (URL encoded string). + :param body: Existing request body (URL encoded string) to embed parameters + into. This may contain extra paramters. Default ''. :param scope: List of scopes to request. Must be equal to or a subset of the scopes granted when obtaining the refresh diff --git a/oauthlib/oauth2/rfc6749/clients/legacy_application.py b/oauthlib/oauth2/rfc6749/clients/legacy_application.py index b16fc9f8..8f036952 100644 --- a/oauthlib/oauth2/rfc6749/clients/legacy_application.py +++ b/oauthlib/oauth2/rfc6749/clients/legacy_application.py @@ -47,6 +47,8 @@ def prepare_request_body(self, username, password, body='', scope=None, **kwargs :param username: The resource owner username. :param password: The resource owner password. + :param body: Existing request body (URL encoded string) to embed parameters + into. This may contain extra paramters. Default ''. :param scope: The scope of the access request as described by `Section 3.3`_. :param kwargs: Extra credentials to include in the token request. diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index 3045676b..6bb784e1 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -97,18 +97,19 @@ def prepare_request_body(self, :param issued_at: A unix timestamp of when the JWT was created. Defaults to now, i.e. ``time.time()``. + :param extra_claims: A dict of additional claims to include in the JWT. + + :param body: Existing request body (URL encoded string) to embed parameters + into. This may contain extra paramters. Default ''. + + :param scope: The scope of the access request. + :param not_before: A unix timestamp after which the JWT may be used. Not included unless provided. :param jwt_id: A unique JWT token identifier. Not included unless provided. - :param extra_claims: A dict of additional claims to include in the JWT. - - :param scope: The scope of the access request. - - :param body: Request body (string) with extra parameters. - :param kwargs: Extra credentials to include in the token request. The "scope" parameter may be used, as defined in the Assertion diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index c14a5f8a..ec59b319 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -8,6 +8,8 @@ """ from __future__ import absolute_import, unicode_literals +import warnings + from ..parameters import (parse_authorization_code_response, parse_token_response, prepare_grant_uri, prepare_token_request) @@ -85,17 +87,14 @@ def prepare_request_uri(self, uri, redirect_uri=None, scope=None, return prepare_grant_uri(uri, self.client_id, 'code', redirect_uri=redirect_uri, scope=scope, state=state, **kwargs) - def prepare_request_body(self, client_id=None, code=None, body='', - redirect_uri=None, **kwargs): + def prepare_request_body(self, code=None, redirect_uri=None, body='', + include_client_id=True, **kwargs): """Prepare the access token request body. The client makes a request to the token endpoint by adding the following parameters using the "application/x-www-form-urlencoded" format in the HTTP request entity-body: - :param client_id: REQUIRED, if the client is not authenticating with the - authorization server as described in `Section 3.2.1`_. - :param code: REQUIRED. The authorization code received from the authorization server. @@ -103,6 +102,15 @@ def prepare_request_body(self, client_id=None, code=None, body='', authorization request as described in `Section 4.1.1`_, and their values MUST be identical. + :param body: Existing request body (URL encoded string) to embed parameters + into. This may contain extra paramters. Default ''. + + :param include_client_id: `True` (default) to send the `client_id` in the + body of the upstream request. This is required + if the client is not authenticating with the + authorization server as described in `Section 3.2.1`_. + :type include_client_id: Boolean + :param kwargs: Extra parameters to include in the token request. In addition OAuthLib will add the ``grant_type`` parameter set to @@ -124,8 +132,18 @@ def prepare_request_body(self, client_id=None, code=None, body='', .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ code = code or self.code + if 'client_id' in kwargs: + warnings.warn("`client_id` has been deprecated in favor of " + "`include_client_id`, a boolean value which will " + "include the already configured `self.client_id`.", + DeprecationWarning) + if kwargs['client_id'] != self.client_id: + raise ValueError("`client_id` was supplied as an argument, but " + "it does not match `self.client_id`") + if include_client_id: + kwargs['client_id'] = self.client_id return prepare_token_request('authorization_code', code=code, body=body, - client_id=client_id, redirect_uri=redirect_uri, **kwargs) + redirect_uri=redirect_uri, **kwargs) def parse_request_uri_response(self, uri, state=None): """Parse the URI query for code and state. diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 74a15f9a..2b6e854e 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -96,7 +96,8 @@ def prepare_token_request(grant_type, body='', **kwargs): :param grant_type: To indicate grant type being used, i.e. "password", "authorization_code" or "client_credentials". - :param body: Existing request body to embed parameters in. + :param body: Existing request body (URL encoded string) to embed parameters + into. This may contain extra paramters. Default ''. :param code: If using authorization code grant, pass the previously obtained authorization code as the ``code`` argument. :param redirect_uri: If the "redirect_uri" parameter was included in the @@ -119,6 +120,7 @@ def prepare_token_request(grant_type, body='', **kwargs): kwargs['scope'] = list_to_scope(kwargs['scope']) for k in kwargs: + # this handles: `code`, `redirect_uri`, or undocumented params if kwargs[k]: params.append((unicode_type(k), kwargs[k])) diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 4ecc3b30..d87005a2 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -3,6 +3,7 @@ import datetime import os +import warnings from mock import patch @@ -38,7 +39,7 @@ class WebApplicationClientTest(TestCase): code = "zzzzaaaa" body = "not=empty" - body_code = "not=empty&grant_type=authorization_code&code=%s" % code + body_code = "not=empty&grant_type=authorization_code&code=%s&client_id=%s" % (code, client_id) body_redirect = body_code + "&redirect_uri=http%3A%2F%2Fmy.page.com%2Fcallback" body_kwargs = body_code + "&some=providers&require=extra+arguments" @@ -177,3 +178,49 @@ def test_prepare_authorization_requeset(self): # verify default header and body only self.assertEqual(header, {'Content-Type': 'application/x-www-form-urlencoded'}) self.assertEqual(body, '') + + def test_prepare_request_body(self): + """ + see issue #585 + https://github.com/oauthlib/oauthlib/issues/585 + + `prepare_request_body` should support the following scenarios: + 1. Include client_id alone in the body (default) + 2. Include client_id and client_secret in auth and not include them in the body (RFC preferred solution) + 3. Include client_id and client_secret in the body (RFC alternative solution) + """ + client = WebApplicationClient(self.client_id) + + # scenario 1, default behavior to include `client_id` + r1 = client.prepare_request_body() + self.assertEqual(r1, 'grant_type=authorization_code&client_id=someclientid') + + r1b = client.prepare_request_body(include_client_id=True) + self.assertEqual(r1b, 'grant_type=authorization_code&client_id=someclientid') + + # scenario 2, do not include `client_id` in the body, so it can be sent in auth. + r2 = client.prepare_request_body(include_client_id=False) + self.assertEqual(r2, 'grant_type=authorization_code') + + # scenario 3, Include client_id and client_secret in the body (RFC alternative solution) + r3 = client.prepare_request_body(client_secret='secret') + self.assertEqual(r3, 'grant_type=authorization_code&client_secret=secret&client_id=someclientid') + r3b = client.prepare_request_body(include_client_id=True, client_secret='secret') + self.assertEqual(r3b, 'grant_type=authorization_code&client_secret=secret&client_id=someclientid') + + # scenario Warnings + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") # catch all + + # warning1 - raise a DeprecationWarning if a `client_id` is submitted + rWarnings1 = client.prepare_request_body(client_id=self.client_id) + self.assertEqual(len(w), 1) + self.assertIsInstance(w[0].message, DeprecationWarning) + self.assertEqual(w[0].message.message, """`client_id` has been deprecated in favor of `include_client_id`, a boolean value which will include the already configured `self.client_id`.""") + + # scenario Exceptions + # exception1 - raise a ValueError if the a different `client_id` is submitted + with self.assertRaises(ValueError) as cm: + client.prepare_request_body(client_id='different_client_id') + self.assertEqual(cm.exception.message, + "`client_id` was supplied as an argument, but it does not match `self.client_id`") From e4658e048ac2b2126b56c41e0494cd17607ea190 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Thu, 13 Sep 2018 15:44:05 -0400 Subject: [PATCH 093/237] updated tests to pass on 2.x and 3.x --- .../rfc6749/clients/test_web_application.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index d87005a2..702fcf22 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -203,10 +203,15 @@ def test_prepare_request_body(self): self.assertEqual(r2, 'grant_type=authorization_code') # scenario 3, Include client_id and client_secret in the body (RFC alternative solution) + # the order of kwargs being appended is not guaranteed. for brevity, check the 2 permutations instead of sorting r3 = client.prepare_request_body(client_secret='secret') - self.assertEqual(r3, 'grant_type=authorization_code&client_secret=secret&client_id=someclientid') + self.assertIn(r3, ('grant_type=authorization_code&client_secret=secret&client_id=someclientid', + 'grant_type=authorization_code&client_id=someclientid&client_secret=secret',) + ) r3b = client.prepare_request_body(include_client_id=True, client_secret='secret') - self.assertEqual(r3b, 'grant_type=authorization_code&client_secret=secret&client_id=someclientid') + self.assertIn(r3b, ('grant_type=authorization_code&client_secret=secret&client_id=someclientid', + 'grant_type=authorization_code&client_id=someclientid&client_secret=secret',) + ) # scenario Warnings with warnings.catch_warnings(record=True) as w: @@ -216,11 +221,11 @@ def test_prepare_request_body(self): rWarnings1 = client.prepare_request_body(client_id=self.client_id) self.assertEqual(len(w), 1) self.assertIsInstance(w[0].message, DeprecationWarning) - self.assertEqual(w[0].message.message, """`client_id` has been deprecated in favor of `include_client_id`, a boolean value which will include the already configured `self.client_id`.""") + + # testing the exact warning message in Python2&Python3 is a pain # scenario Exceptions # exception1 - raise a ValueError if the a different `client_id` is submitted with self.assertRaises(ValueError) as cm: client.prepare_request_body(client_id='different_client_id') - self.assertEqual(cm.exception.message, - "`client_id` was supplied as an argument, but it does not match `self.client_id`") + # testing the exact exception message in Python2&Python3 is a pain From aef9a3e944f41c3afaaf22ba20f86a267a7d3bb3 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 14 Sep 2018 05:10:25 -0700 Subject: [PATCH 094/237] Prefer assertIsInstance(...) over assertTrue(isinstance(...)) It is a more explicit assert with a more information message in case of failure. For a full list of available assert methods, see: https://docs.python.org/3/library/unittest.html#assert-methods --- tests/oauth1/rfc5849/test_client.py | 2 +- .../connect/core/grant_types/test_dispatchers.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/oauth1/rfc5849/test_client.py b/tests/oauth1/rfc5849/test_client.py index 777efc2d..069eefd9 100644 --- a/tests/oauth1/rfc5849/test_client.py +++ b/tests/oauth1/rfc5849/test_client.py @@ -39,7 +39,7 @@ class ClientConstructorTests(TestCase): def test_convert_to_unicode_resource_owner(self): client = Client('client-key', resource_owner_key=b'owner key') - self.assertFalse(isinstance(client.resource_owner_key, bytes_type)) + self.assertNotIsInstance(client.resource_owner_key, bytes_type) self.assertEqual(client.resource_owner_key, 'owner key') def test_give_explicit_timestamp(self): diff --git a/tests/openid/connect/core/grant_types/test_dispatchers.py b/tests/openid/connect/core/grant_types/test_dispatchers.py index f90ec468..84f26880 100644 --- a/tests/openid/connect/core/grant_types/test_dispatchers.py +++ b/tests/openid/connect/core/grant_types/test_dispatchers.py @@ -36,23 +36,23 @@ def test_create_authorization_response_openid(self): self.request.scopes = ('hello', 'openid') self.request.response_type = 'id_token' handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, ImplicitGrant)) + self.assertIsInstance(handler, ImplicitGrant) def test_validate_authorization_request_openid(self): self.request.scopes = ('hello', 'openid') self.request.response_type = 'id_token' handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, ImplicitGrant)) + self.assertIsInstance(handler, ImplicitGrant) def test_create_authorization_response_oauth(self): self.request.scopes = ('hello', 'world') handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, ImplicitGrant)) + self.assertIsInstance(handler, ImplicitGrant) def test_validate_authorization_request_oauth(self): self.request.scopes = ('hello', 'world') handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, ImplicitGrant)) + self.assertIsInstance(handler, ImplicitGrant) class DispatcherTest(TestCase): @@ -82,7 +82,7 @@ def setUp(self): def test_create_token_response_openid(self): handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, AuthorizationCodeGrant)) + self.assertIsInstance(handler, AuthorizationCodeGrant) self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) @@ -104,7 +104,7 @@ def setUp(self): def test_create_token_response_openid_without_code(self): handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, OAuth2AuthorizationCodeGrant)) + self.assertIsInstance(handler, OAuth2AuthorizationCodeGrant) self.assertFalse(self.dispatcher.request_validator.get_authorization_code_scopes.called) @@ -121,5 +121,5 @@ def setUp(self): def test_create_token_response_oauth(self): handler = self.dispatcher._handler_for_request(self.request) - self.assertTrue(isinstance(handler, OAuth2AuthorizationCodeGrant)) + self.assertIsInstance(handler, OAuth2AuthorizationCodeGrant) self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called) From 8aa89569f14b493ba2672d7a64c7c1c138c82c3b Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 14 Sep 2018 05:07:46 -0700 Subject: [PATCH 095/237] Remove unnecessary workaround for bytes type The type 'bytes' is available on all supported Pythons. Likewise the byte literal b'...' is available on all supported Pythons. Use idiomatic Python and remove workaround for an issue that no longer exists. Makes the code more forward compatible with Python 3. --- oauthlib/common.py | 14 ++++++-------- oauthlib/oauth1/rfc5849/__init__.py | 5 ----- oauthlib/oauth1/rfc5849/signature.py | 4 ++-- oauthlib/oauth1/rfc5849/utils.py | 2 +- tests/oauth1/rfc5849/test_client.py | 12 ++++++------ tests/test_common.py | 13 ++++--------- 6 files changed, 19 insertions(+), 31 deletions(-) diff --git a/oauthlib/common.py b/oauthlib/common.py index 63647616..bd6ec56f 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -54,10 +54,8 @@ if PY3: unicode_type = str - bytes_type = bytes else: unicode_type = unicode - bytes_type = str # 'safe' must be bytes (Python 2.6 requires bytes, other versions allow either) @@ -66,7 +64,7 @@ def quote(s, safe=b'/'): s = _quote(s, safe) # PY3 always returns unicode. PY2 may return either, depending on whether # it had to modify the string. - if isinstance(s, bytes_type): + if isinstance(s, bytes): s = s.decode('utf-8') return s @@ -76,7 +74,7 @@ def unquote(s): # PY3 always returns unicode. PY2 seems to always return what you give it, # which differs from quote's behavior. Just to be safe, make sure it is # unicode before we return. - if isinstance(s, bytes_type): + if isinstance(s, bytes): s = s.decode('utf-8') return s @@ -109,8 +107,8 @@ def decode_params_utf8(params): decoded = [] for k, v in params: decoded.append(( - k.decode('utf-8') if isinstance(k, bytes_type) else k, - v.decode('utf-8') if isinstance(v, bytes_type) else v)) + k.decode('utf-8') if isinstance(k, bytes) else k, + v.decode('utf-8') if isinstance(v, bytes) else v)) return decoded @@ -174,7 +172,7 @@ def extract_params(raw): empty list of parameters. Any other input will result in a return value of None. """ - if isinstance(raw, bytes_type) or isinstance(raw, unicode_type): + if isinstance(raw, bytes) or isinstance(raw, unicode_type): try: params = urldecode(raw) except ValueError: @@ -309,7 +307,7 @@ def to_unicode(data, encoding='UTF-8'): if isinstance(data, unicode_type): return data - if isinstance(data, bytes_type): + if isinstance(data, bytes): return unicode_type(data, encoding=encoding) if hasattr(data, '__iter__'): diff --git a/oauthlib/oauth1/rfc5849/__init__.py b/oauthlib/oauth1/rfc5849/__init__.py index 87a8e6ba..887ab69f 100644 --- a/oauthlib/oauth1/rfc5849/__init__.py +++ b/oauthlib/oauth1/rfc5849/__init__.py @@ -18,11 +18,6 @@ except ImportError: import urllib.parse as urlparse -if sys.version_info[0] == 3: - bytes_type = bytes -else: - bytes_type = str - from oauthlib.common import Request, urlencode, generate_nonce from oauthlib.common import generate_timestamp, to_unicode from . import parameters, signature diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index 4e672ba3..e90d6f37 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -28,7 +28,7 @@ import hmac import logging -from oauthlib.common import (bytes_type, extract_params, safe_string_equals, +from oauthlib.common import (extract_params, safe_string_equals, unicode_type, urldecode) from . import utils @@ -635,7 +635,7 @@ def verify_hmac_sha1(request, client_secret=None, def _prepare_key_plus(alg, keystr): - if isinstance(keystr, bytes_type): + if isinstance(keystr, bytes): keystr = keystr.decode('utf-8') return alg.prepare_key(keystr) diff --git a/oauthlib/oauth1/rfc5849/utils.py b/oauthlib/oauth1/rfc5849/utils.py index 3762e3b5..735f21d6 100644 --- a/oauthlib/oauth1/rfc5849/utils.py +++ b/oauthlib/oauth1/rfc5849/utils.py @@ -8,7 +8,7 @@ """ from __future__ import absolute_import, unicode_literals -from oauthlib.common import bytes_type, quote, unicode_type, unquote +from oauthlib.common import quote, unicode_type, unquote try: import urllib2 diff --git a/tests/oauth1/rfc5849/test_client.py b/tests/oauth1/rfc5849/test_client.py index 777efc2d..ab6f8da0 100644 --- a/tests/oauth1/rfc5849/test_client.py +++ b/tests/oauth1/rfc5849/test_client.py @@ -5,7 +5,7 @@ from oauthlib.oauth1 import (SIGNATURE_PLAINTEXT, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_RSA, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY) -from oauthlib.oauth1.rfc5849 import Client, bytes_type +from oauthlib.oauth1.rfc5849 import Client from ...unittest import TestCase @@ -39,7 +39,7 @@ class ClientConstructorTests(TestCase): def test_convert_to_unicode_resource_owner(self): client = Client('client-key', resource_owner_key=b'owner key') - self.assertFalse(isinstance(client.resource_owner_key, bytes_type)) + self.assertFalse(isinstance(client.resource_owner_key, bytes)) self.assertEqual(client.resource_owner_key, 'owner key') def test_give_explicit_timestamp(self): @@ -57,11 +57,11 @@ def test_decoding(self): uri, headers, body = client.sign('http://a.b/path?query', http_method='POST', body='a=b', headers={'Content-Type': 'application/x-www-form-urlencoded'}) - self.assertIsInstance(uri, bytes_type) - self.assertIsInstance(body, bytes_type) + self.assertIsInstance(uri, bytes) + self.assertIsInstance(body, bytes) for k, v in headers.items(): - self.assertIsInstance(k, bytes_type) - self.assertIsInstance(v, bytes_type) + self.assertIsInstance(k, bytes) + self.assertIsInstance(v, bytes) def test_hmac_sha1(self): client = Client('client_key') diff --git a/tests/test_common.py b/tests/test_common.py index f239368d..20d9f5b7 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -10,11 +10,6 @@ from .unittest import TestCase -if sys.version_info[0] == 3: - bytes_type = bytes -else: - bytes_type = lambda s, e: str(s) - PARAMS_DICT = {'foo': 'bar', 'baz': '123', } PARAMS_TWOTUPLE = [('foo', 'bar'), ('baz', '123')] PARAMS_FORMENCODED = 'foo=bar&baz=123' @@ -122,11 +117,11 @@ class RequestTest(TestCase): def test_non_unicode_params(self): r = Request( - bytes_type('http://a.b/path?query', 'utf-8'), - http_method=bytes_type('GET', 'utf-8'), - body=bytes_type('you=shall+pass', 'utf-8'), + b'http://a.b/path?query', + http_method=b'GET', + body=b'you=shall+pass', headers={ - bytes_type('a', 'utf-8'): bytes_type('b', 'utf-8') + b'a': b'b', } ) self.assertEqual(r.uri, 'http://a.b/path?query') From a0f38f71cb8764bbff8dd2cdac5031a09086665e Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 14 Sep 2018 12:58:16 -0700 Subject: [PATCH 096/237] Correct recent merge (#596) Merge c8a7cb199a8d448c2934100a5bb06598be402939 mistakenly reverted a line from aef9a3e944f41c3afaaf22ba20f86a267a7d3bb3. --- tests/oauth1/rfc5849/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/oauth1/rfc5849/test_client.py b/tests/oauth1/rfc5849/test_client.py index ab6f8da0..e1f83deb 100644 --- a/tests/oauth1/rfc5849/test_client.py +++ b/tests/oauth1/rfc5849/test_client.py @@ -39,7 +39,7 @@ class ClientConstructorTests(TestCase): def test_convert_to_unicode_resource_owner(self): client = Client('client-key', resource_owner_key=b'owner key') - self.assertFalse(isinstance(client.resource_owner_key, bytes)) + self.assertNotIsInstance(client.resource_owner_key, bytes) self.assertEqual(client.resource_owner_key, 'owner key') def test_give_explicit_timestamp(self): From c8fcbf87ca38faa4dfbe56d0609a4ce15c2d7aca Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Fri, 14 Sep 2018 19:08:56 -0400 Subject: [PATCH 097/237] standardized some test values integrated against requests_oauthlib idea --- oauthlib/oauth2/rfc6749/parameters.py | 2 +- .../rfc6749/clients/test_legacy_application.py | 4 ++-- tests/oauth2/rfc6749/clients/test_web_application.py | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 2b6e854e..1229f314 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -146,7 +146,7 @@ def prepare_token_revocation_request(url, token, token_type_hint="access_token", token types. An authorization server MAY ignore this parameter, particularly if it is able to detect the token type automatically. - + This specification defines two values for `token_type_hint`: * access_token: An access token as defined in [RFC6749], diff --git a/tests/oauth2/rfc6749/clients/test_legacy_application.py b/tests/oauth2/rfc6749/clients/test_legacy_application.py index 3f97c027..1e11112c 100644 --- a/tests/oauth2/rfc6749/clients/test_legacy_application.py +++ b/tests/oauth2/rfc6749/clients/test_legacy_application.py @@ -21,8 +21,8 @@ class LegacyApplicationClientTest(TestCase): "require": "extra arguments" } - username = "foo" - password = "bar" + username = "user_username" + password = "user_password" body = "not=empty" body_up = "not=empty&grant_type=password&username=%s&password=%s" % (username, password) diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 702fcf22..91446591 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -204,13 +204,13 @@ def test_prepare_request_body(self): # scenario 3, Include client_id and client_secret in the body (RFC alternative solution) # the order of kwargs being appended is not guaranteed. for brevity, check the 2 permutations instead of sorting - r3 = client.prepare_request_body(client_secret='secret') - self.assertIn(r3, ('grant_type=authorization_code&client_secret=secret&client_id=someclientid', - 'grant_type=authorization_code&client_id=someclientid&client_secret=secret',) + r3 = client.prepare_request_body(client_secret='someclientsecret') + self.assertIn(r3, ('grant_type=authorization_code&client_secret=someclientsecret&client_id=someclientid', + 'grant_type=authorization_code&client_id=someclientid&client_secret=someclientsecret',) ) - r3b = client.prepare_request_body(include_client_id=True, client_secret='secret') - self.assertIn(r3b, ('grant_type=authorization_code&client_secret=secret&client_id=someclientid', - 'grant_type=authorization_code&client_id=someclientid&client_secret=secret',) + r3b = client.prepare_request_body(include_client_id=True, client_secret='someclientsecret') + self.assertIn(r3b, ('grant_type=authorization_code&client_secret=someclientsecret&client_id=someclientid', + 'grant_type=authorization_code&client_id=someclientid&client_secret=someclientsecret',) ) # scenario Warnings From 037453c6f92b502eaae2acafe11161e4bb2e38bb Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 14 Sep 2018 16:17:25 -0700 Subject: [PATCH 098/237] Remove unmaintained nose dependency from tests The nose project has ceased development. From their docs page: https://nose.readthedocs.io/ > Note to Users > > Nose has been in maintenance mode for the past several years and will > likely cease without a new person/team to take over maintainership. > New projects should consider using Nose2, py.test, or just plain > unittest/unittest2. Simplify test infrastructure by using the stdlib unittest discover command. One fewer dependency. --- requirements-test.txt | 1 - setup.py | 3 +-- tox.ini | 5 ++++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 5bf6e065..c3e0a7ba 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,3 @@ -r requirements.txt coverage>=3.7.1 -nose==1.3.7 mock>=2.0 diff --git a/setup.py b/setup.py index 17dcd7b3..640bbe1c 100755 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ def fread(fn): with open(join(dirname(__file__), fn), 'r') as f: return f.read() -tests_require = ['nose', 'cryptography', 'pyjwt>=1.0.0', 'blinker'] +tests_require = ['cryptography', 'pyjwt>=1.0.0', 'blinker'] if sys.version_info[0] == 2: tests_require.append('mock>=2.0') rsa_require = ['cryptography'] @@ -40,7 +40,6 @@ def fread(fn): platforms='any', license='BSD', packages=find_packages(exclude=('docs', 'tests', 'tests.*')), - test_suite='nose.collector', tests_require=tests_require, extras_require={ 'test': tests_require, diff --git a/tox.ini b/tox.ini index b736cf1f..6f6c18ca 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,10 @@ envlist = py27,py34,py35,py36,pypy,docs,readme [testenv] deps= -rrequirements-test.txt -commands=nosetests -s --with-coverage --cover-html --cover-html-dir={toxinidir}/htmlcov-{envname} --cover-erase --cover-package=oauthlib -w tests +commands= + coverage run --source oauthlib -m unittest discover + coverage report + # tox -e docs to mimick readthedocs build. # as of today, RTD is using python2.7 and doesn't run "setup.py install" From 50eccb87b4119c6e98fafc9e38ea3276bc007942 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Sat, 15 Sep 2018 21:09:36 +0100 Subject: [PATCH 099/237] Remove last remaining G+ reference. (#598) (Cherry picked from f3ae98cef91e140b10d25fbd496622d879cc0c0c) --- docs/index.rst | 6 +++--- docs/oauth2/endpoints/endpoints.rst | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 1da2ca57..b6ce191e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,14 +7,14 @@ Welcome to OAuthLib's documentation! ==================================== If you can't find what you need or have suggestions for improvement, don't -hesitate to open a `new issue on GitHub`_! +hesitate to open a `new issue on GitHub`_! Check out :doc:`error_reporting` for details on how to be an awesome bug reporter. -For news and discussions please head over to our `G+ OAuthLib community`_. +For news and discussions please head over to our `Gitter OAuthLib community`_. .. _`new issue on GitHub`: https://github.com/oauthlib/oauthlib/issues/new -.. _`G+ OAuthLib community`: https://plus.google.com/communities/101889017375384052571 +.. _`Gitter OAuthLib community`: https://gitter.im/oauthlib/Lobby .. toctree:: :maxdepth: 1 diff --git a/docs/oauth2/endpoints/endpoints.rst b/docs/oauth2/endpoints/endpoints.rst index 80d5fbeb..98599e87 100644 --- a/docs/oauth2/endpoints/endpoints.rst +++ b/docs/oauth2/endpoints/endpoints.rst @@ -24,7 +24,7 @@ handles user authorization, the token endpoint which provides tokens and the resource endpoint which provides access to protected resources. It is to the endpoints you will feed requests and get back an almost complete response. This process is simplified for you using a decorator such as the django one described -later (but it's applicable to all other web frameworks librairies). +later (but it's applicable to all other web frameworks libraries). The main purpose of the endpoint in OAuthLib is to figure out which grant type or token to dispatch the request to. From c4903229d99b5d694d4256efc187ac452579811a Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 16 Sep 2018 16:55:41 -0700 Subject: [PATCH 100/237] Correct capitalization of PyPI As spelled on https://pypi.org/. --- README.rst | 2 +- docs/installation.rst | 2 +- tox.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index a84307f2..7c41a80c 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ logic for Python 2.7 and 3.4+.* :alt: Coveralls .. image:: https://img.shields.io/pypi/pyversions/oauthlib.svg :target: https://pypi.org/project/oauthlib/ - :alt: Download from PyPi + :alt: Download from PyPI .. image:: https://img.shields.io/pypi/l/oauthlib.svg :target: https://pypi.org/project/oauthlib/ :alt: License diff --git a/docs/installation.rst b/docs/installation.rst index 48e42887..72d7b082 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -9,7 +9,7 @@ For various reasons you may wish to install using your OS packaging system and install instructions for a few are shown below. Please send a PR to add a missing one. -Latest release on PYPI +Latest release on PyPI ---------------------- diff --git a/tox.ini b/tox.ini index 6f6c18ca..c45f6574 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ changedir=docs whitelist_externals=make commands=make clean html -# tox -e readme to mimick pypi long_description check +# tox -e readme to mimick PyPI long_description check [testenv:readme] skipsdist=True deps=readme From e7bd936434f7268b0453fd25c637034f7efd8168 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Mon, 17 Sep 2018 13:18:36 -0400 Subject: [PATCH 101/237] * added support for empty strings of `client_secret` * added LegacyApplicationClient tests to ensure the grant supports a variety of allowed methods --- CHANGELOG.rst | 10 +++++++ .../oauth2/rfc6749/clients/web_application.py | 8 +++++ oauthlib/oauth2/rfc6749/parameters.py | 4 +++ .../clients/test_legacy_application.py | 28 +++++++++++++++++ .../rfc6749/clients/test_web_application.py | 30 ++++++++++++------- 5 files changed, 70 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a8e1941d..fd537691 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,16 @@ Changelog ========= +Unreleased +------------------ + +* OAuth2's `prepare_token_request` supports sending an empty string for `client_id` (#585) +* OAuth2's `WebApplicationClient.prepare_request_body` was refactored to better + support sending or omitting the `client_id` via a new `include_client_id` kwarg. + By default this is included. The method will also emit a DeprecationWarning if + a `client_id` parameter is submitted; the already configured `self.client_id` + is the preferred option. (#585) + 2.1.0 (2018-05-21) ------------------ diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index ec59b319..b4b109e2 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -128,6 +128,14 @@ def prepare_request_body(self, code=None, redirect_uri=None, body='', >>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar') 'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar' + `Section 3.2.1` also states: + In the "authorization_code" "grant_type" request to the token + endpoint, an unauthenticated client MUST send its "client_id" to + prevent itself from inadvertently accepting a code intended for a + client with a different "client_id". This protects the client from + substitution of the authentication code. (It provides no additional + security for the protected resource.) + .. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1 .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 1229f314..0a36e531 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -124,6 +124,10 @@ def prepare_token_request(grant_type, body='', **kwargs): if kwargs[k]: params.append((unicode_type(k), kwargs[k])) + if ('client_secret' in kwargs) and ('client_secret' not in params): + if kwargs['client_secret'] == '': + params.append((unicode_type('client_secret'), kwargs['client_secret'])) + return add_params_to_qs(body, params) diff --git a/tests/oauth2/rfc6749/clients/test_legacy_application.py b/tests/oauth2/rfc6749/clients/test_legacy_application.py index 1e11112c..4e518e84 100644 --- a/tests/oauth2/rfc6749/clients/test_legacy_application.py +++ b/tests/oauth2/rfc6749/clients/test_legacy_application.py @@ -15,6 +15,7 @@ class LegacyApplicationClientTest(TestCase): client_id = "someclientid" + client_secret = 'someclientsecret' scope = ["/profile"] kwargs = { "some": "providers", @@ -88,3 +89,30 @@ def record_scope_change(sender, message, old, new): finally: signals.scope_changed.disconnect(record_scope_change) del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] + + def test_prepare_request_body(self): + """ + see issue #585 + https://github.com/oauthlib/oauthlib/issues/585 + """ + client = LegacyApplicationClient(self.client_id) + + # scenario 1, default behavior to not include `client_id` + r1 = client.prepare_request_body(username=self.username, password=self.password) + self.assertEqual(r1, 'grant_type=password&username=user_username&password=user_password') + + # scenario 2, include `client_id` in the body + r2 = client.prepare_request_body(username=self.username, password=self.password, client_id=self.client_id) + self.assertEqual(r2, 'grant_type=password&username=user_username&password=user_password&client_id=%s' % self.client_id) + + # scenario 3, include `client_id` + `client_secret` in the body + r3 = client.prepare_request_body(username=self.username, password=self.password, client_id=self.client_id, client_secret=self.client_secret) + self.assertEqual(r3, 'grant_type=password&username=user_username&password=user_password&client_id=%s&client_secret=%s' % (self.client_id, self.client_secret)) + + # scenario 4, `client_secret` is an empty string + r4 = client.prepare_request_body(username=self.username, password=self.password, client_id=self.client_id, client_secret='') + self.assertEqual(r4, 'grant_type=password&username=user_username&password=user_password&client_id=%s&client_secret=%s' % (self.client_id, '')) + + # scenario 4b`,` client_secret is `None` + r4b = client.prepare_request_body(username=self.username, password=self.password, client_id=self.client_id, client_secret=None) + self.assertEqual(r4b, 'grant_type=password&username=user_username&password=user_password&client_id=%s' % (self.client_id, )) diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 91446591..fb800f74 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -21,6 +21,7 @@ class WebApplicationClientTest(TestCase): client_id = "someclientid" + client_secret = 'someclientsecret' uri = "https://example.com/path?query=world" uri_id = uri + "&response_type=code&client_id=" + client_id uri_redirect = uri_id + "&redirect_uri=http%3A%2F%2Fmy.page.com%2Fcallback" @@ -188,15 +189,16 @@ def test_prepare_request_body(self): 1. Include client_id alone in the body (default) 2. Include client_id and client_secret in auth and not include them in the body (RFC preferred solution) 3. Include client_id and client_secret in the body (RFC alternative solution) + 4. Include client_id in the body and an empty string for client_secret. """ client = WebApplicationClient(self.client_id) # scenario 1, default behavior to include `client_id` r1 = client.prepare_request_body() - self.assertEqual(r1, 'grant_type=authorization_code&client_id=someclientid') + self.assertEqual(r1, 'grant_type=authorization_code&client_id=%s' % self.client_id) r1b = client.prepare_request_body(include_client_id=True) - self.assertEqual(r1b, 'grant_type=authorization_code&client_id=someclientid') + self.assertEqual(r1b, 'grant_type=authorization_code&client_id=%s' % self.client_id) # scenario 2, do not include `client_id` in the body, so it can be sent in auth. r2 = client.prepare_request_body(include_client_id=False) @@ -204,14 +206,22 @@ def test_prepare_request_body(self): # scenario 3, Include client_id and client_secret in the body (RFC alternative solution) # the order of kwargs being appended is not guaranteed. for brevity, check the 2 permutations instead of sorting - r3 = client.prepare_request_body(client_secret='someclientsecret') - self.assertIn(r3, ('grant_type=authorization_code&client_secret=someclientsecret&client_id=someclientid', - 'grant_type=authorization_code&client_id=someclientid&client_secret=someclientsecret',) - ) - r3b = client.prepare_request_body(include_client_id=True, client_secret='someclientsecret') - self.assertIn(r3b, ('grant_type=authorization_code&client_secret=someclientsecret&client_id=someclientid', - 'grant_type=authorization_code&client_id=someclientid&client_secret=someclientsecret',) - ) + r3 = client.prepare_request_body(client_secret=self.client_secret) + self.assertIn(r3, ('grant_type=authorization_code&client_secret=%s&client_id=%s' % (self.client_secret, self.client_id, ), + 'grant_type=authorization_code&client_id=%s&client_secret=%s' % (self.client_id, self.client_secret, ), + )) + r3b = client.prepare_request_body(include_client_id=True, client_secret=self.client_secret) + self.assertIn(r3b, ('grant_type=authorization_code&client_secret=%s&client_id=%s' % (self.client_secret, self.client_id, ), + 'grant_type=authorization_code&client_id=%s&client_secret=%s' % (self.client_id, self.client_secret, ), + )) + + # scenario 4, `client_secret` is an empty string + r4 = client.prepare_request_body(include_client_id=True, client_secret='') + self.assertEqual(r4, 'grant_type=authorization_code&client_id=%s&client_secret=' % self.client_id) + + # scenario 4b, `client_secret` is `None` + r4b = client.prepare_request_body(include_client_id=True, client_secret=None) + self.assertEqual(r4b, 'grant_type=authorization_code&client_id=%s' % self.client_id) # scenario Warnings with warnings.catch_warnings(record=True) as w: From b4ceb8a7fae065817f86c259dfbf91344ecb2925 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Mon, 17 Sep 2018 19:54:37 -0400 Subject: [PATCH 102/237] migrated `include_client_id` to `prepare_request_token` --- .../rfc6749/clients/backend_application.py | 13 ++++- .../rfc6749/clients/legacy_application.py | 11 ++++- .../rfc6749/clients/service_application.py | 20 ++++++-- .../oauth2/rfc6749/clients/web_application.py | 5 +- oauthlib/oauth2/rfc6749/parameters.py | 32 +++++++++++-- .../clients/test_backend_application.py | 1 + .../clients/test_legacy_application.py | 48 +++++++++++++++---- .../rfc6749/clients/test_web_application.py | 35 ++++++++++---- 8 files changed, 137 insertions(+), 28 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py index 99dbfc5d..cd46f128 100644 --- a/oauthlib/oauth2/rfc6749/clients/backend_application.py +++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py @@ -30,7 +30,8 @@ class BackendApplicationClient(Client): no additional authorization request is needed. """ - def prepare_request_body(self, body='', scope=None, **kwargs): + def prepare_request_body(self, body='', scope=None, + include_client_id=None, **kwargs): """Add the client credentials to the request body. The client makes a request to the token endpoint by adding the @@ -41,6 +42,14 @@ def prepare_request_body(self, body='', scope=None, **kwargs): into. This may contain extra paramters. Default ''. :param scope: The scope of the access request as described by `Section 3.3`_. + + :param include_client_id: `True` to send the `client_id` in the body of + the upstream request. Default `None`. This is + required if the client is not authenticating + with the authorization server as described + in `Section 3.2.1`_. + :type include_client_id: Boolean + :param kwargs: Extra credentials to include in the token request. The client MUST authenticate with the authorization server as @@ -58,5 +67,7 @@ def prepare_request_body(self, body='', scope=None, **kwargs): .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ + kwargs['client_id'] = self.client_id + kwargs['include_client_id'] = include_client_id return prepare_token_request('client_credentials', body=body, scope=scope, **kwargs) diff --git a/oauthlib/oauth2/rfc6749/clients/legacy_application.py b/oauthlib/oauth2/rfc6749/clients/legacy_application.py index 8f036952..a13927a2 100644 --- a/oauthlib/oauth2/rfc6749/clients/legacy_application.py +++ b/oauthlib/oauth2/rfc6749/clients/legacy_application.py @@ -38,7 +38,8 @@ class LegacyApplicationClient(Client): def __init__(self, client_id, **kwargs): super(LegacyApplicationClient, self).__init__(client_id, **kwargs) - def prepare_request_body(self, username, password, body='', scope=None, **kwargs): + def prepare_request_body(self, username, password, body='', scope=None, + include_client_id=None, **kwargs): """Add the resource owner password and username to the request body. The client makes a request to the token endpoint by adding the @@ -51,6 +52,12 @@ def prepare_request_body(self, username, password, body='', scope=None, **kwargs into. This may contain extra paramters. Default ''. :param scope: The scope of the access request as described by `Section 3.3`_. + :param include_client_id: `True` to send the `client_id` in the body of + the upstream request. Default `None`. This is + required if the client is not authenticating + with the authorization server as described + in `Section 3.2.1`_. + :type include_client_id: Boolean :param kwargs: Extra credentials to include in the token request. If the client type is confidential or the client was issued client @@ -70,5 +77,7 @@ def prepare_request_body(self, username, password, body='', scope=None, **kwargs .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ + kwargs['client_id'] = self.client_id + kwargs['include_client_id'] = include_client_id return prepare_token_request('password', body=body, username=username, password=password, scope=scope, **kwargs) diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index 6bb784e1..759e0d21 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -72,7 +72,8 @@ def prepare_request_body(self, issued_at=None, extra_claims=None, body='', - scope=None, + scope=None, + include_client_id=None, **kwargs): """Create and add a JWT assertion to the request body. @@ -104,14 +105,25 @@ def prepare_request_body(self, :param scope: The scope of the access request. + :param include_client_id: `True` to send the `client_id` in the body of + the upstream request. Default `None`. This is + required if the client is not authenticating + with the authorization server as described + in `Section 3.2.1`_. + :type include_client_id: Boolean + :param not_before: A unix timestamp after which the JWT may be used. - Not included unless provided. + Not included unless provided. * :param jwt_id: A unique JWT token identifier. Not included unless - provided. + provided. * :param kwargs: Extra credentials to include in the token request. + Parameters marked with a `*` above are not explicit arguments in the + function definition, but are specially documented arguments for items + appearing in the generic `**kwargs` keyworded input. + The "scope" parameter may be used, as defined in the Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants [I-D.ietf-oauth-assertions] specification, to indicate the requested @@ -169,6 +181,8 @@ def prepare_request_body(self, assertion = jwt.encode(claim, key, 'RS256') assertion = to_unicode(assertion) + kwargs['client_id'] = self.client_id + kwargs['include_client_id'] = include_client_id return prepare_token_request(self.grant_type, body=body, assertion=assertion, diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index b4b109e2..487e3a04 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -148,8 +148,9 @@ def prepare_request_body(self, code=None, redirect_uri=None, body='', if kwargs['client_id'] != self.client_id: raise ValueError("`client_id` was supplied as an argument, but " "it does not match `self.client_id`") - if include_client_id: - kwargs['client_id'] = self.client_id + + kwargs['client_id'] = self.client_id + kwargs['include_client_id'] = include_client_id return prepare_token_request('authorization_code', code=code, body=body, redirect_uri=redirect_uri, **kwargs) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 0a36e531..21c8605b 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -87,7 +87,7 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None, return add_params_to_uri(uri, params) -def prepare_token_request(grant_type, body='', **kwargs): +def prepare_token_request(grant_type, body='', include_client_id=True, **kwargs): """Prepare the access token request. The client makes a request to the token endpoint by adding the @@ -96,15 +96,33 @@ def prepare_token_request(grant_type, body='', **kwargs): :param grant_type: To indicate grant type being used, i.e. "password", "authorization_code" or "client_credentials". + :param body: Existing request body (URL encoded string) to embed parameters into. This may contain extra paramters. Default ''. - :param code: If using authorization code grant, pass the previously - obtained authorization code as the ``code`` argument. + + :param code: If using authorization_code grant, pass the previously + obtained authorization code as the ``code`` argument. + :param redirect_uri: If the "redirect_uri" parameter was included in the authorization request as described in - `Section 4.1.1`_, and their values MUST be identical. + `Section 4.1.1`_, and their values MUST be identical. * + + :param include_client_id: `True` (default) to send the `client_id` in the + body of the upstream request. This is required + if the client is not authenticating with the + authorization server as described in + `Section 3.2.1`_. + :type include_client_id: Boolean + + :param client_id: Unicode client identifier. Will only appear if + `include_client_id` is True. * + :param kwargs: Extra arguments to embed in the request body. + Parameters marked with a `*` above are not explicit arguments in the + function definition, but are specially documented arguments for items + appearing in the generic `**kwargs` keyworded input. + An example of an authorization code token request body: .. code-block:: http @@ -119,6 +137,12 @@ def prepare_token_request(grant_type, body='', **kwargs): if 'scope' in kwargs: kwargs['scope'] = list_to_scope(kwargs['scope']) + # pull the `client_id` out of the kwargs. + client_id = kwargs.pop('client_id', None) + if include_client_id: + if client_id is not None: + params.append((unicode_type('client_id'), client_id)) + for k in kwargs: # this handles: `code`, `redirect_uri`, or undocumented params if kwargs[k]: diff --git a/tests/oauth2/rfc6749/clients/test_backend_application.py b/tests/oauth2/rfc6749/clients/test_backend_application.py index 6b342f0e..aa2ba2bb 100644 --- a/tests/oauth2/rfc6749/clients/test_backend_application.py +++ b/tests/oauth2/rfc6749/clients/test_backend_application.py @@ -15,6 +15,7 @@ class BackendApplicationClientTest(TestCase): client_id = "someclientid" + client_secret = 'someclientsecret' scope = ["/profile"] kwargs = { "some": "providers", diff --git a/tests/oauth2/rfc6749/clients/test_legacy_application.py b/tests/oauth2/rfc6749/clients/test_legacy_application.py index 4e518e84..21af4a30 100644 --- a/tests/oauth2/rfc6749/clients/test_legacy_application.py +++ b/tests/oauth2/rfc6749/clients/test_legacy_application.py @@ -10,6 +10,12 @@ from ....unittest import TestCase +# this is the same import method used in oauthlib/oauth2/rfc6749/parameters.py +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + @patch('time.time', new=lambda: 1000) class LegacyApplicationClientTest(TestCase): @@ -99,20 +105,44 @@ def test_prepare_request_body(self): # scenario 1, default behavior to not include `client_id` r1 = client.prepare_request_body(username=self.username, password=self.password) - self.assertEqual(r1, 'grant_type=password&username=user_username&password=user_password') + self.assertIn(r1, ('grant_type=password&username=%s&password=%s' % (self.username, self.password, ), + 'grant_type=password&password=%s&username=%s' % (self.password, self.username, ), + )) # scenario 2, include `client_id` in the body - r2 = client.prepare_request_body(username=self.username, password=self.password, client_id=self.client_id) - self.assertEqual(r2, 'grant_type=password&username=user_username&password=user_password&client_id=%s' % self.client_id) + r2 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True) + r2_params = dict(urlparse.parse_qsl(r2, keep_blank_values=True)) + self.assertEqual(len(r2_params.keys()), 4) + self.assertEqual(r2_params['grant_type'], 'password') + self.assertEqual(r2_params['username'], self.username) + self.assertEqual(r2_params['password'], self.password) + self.assertEqual(r2_params['client_id'], self.client_id) # scenario 3, include `client_id` + `client_secret` in the body - r3 = client.prepare_request_body(username=self.username, password=self.password, client_id=self.client_id, client_secret=self.client_secret) - self.assertEqual(r3, 'grant_type=password&username=user_username&password=user_password&client_id=%s&client_secret=%s' % (self.client_id, self.client_secret)) + r3 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret=self.client_secret) + r3_params = dict(urlparse.parse_qsl(r3, keep_blank_values=True)) + self.assertEqual(len(r3_params.keys()), 5) + self.assertEqual(r3_params['grant_type'], 'password') + self.assertEqual(r3_params['username'], self.username) + self.assertEqual(r3_params['password'], self.password) + self.assertEqual(r3_params['client_id'], self.client_id) + self.assertEqual(r3_params['client_secret'], self.client_secret) # scenario 4, `client_secret` is an empty string - r4 = client.prepare_request_body(username=self.username, password=self.password, client_id=self.client_id, client_secret='') - self.assertEqual(r4, 'grant_type=password&username=user_username&password=user_password&client_id=%s&client_secret=%s' % (self.client_id, '')) + r4 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret='') + r4_params = dict(urlparse.parse_qsl(r4, keep_blank_values=True)) + self.assertEqual(len(r4_params.keys()), 5) + self.assertEqual(r4_params['grant_type'], 'password') + self.assertEqual(r4_params['username'], self.username) + self.assertEqual(r4_params['password'], self.password) + self.assertEqual(r4_params['client_id'], self.client_id) + self.assertEqual(r4_params['client_secret'], '') # scenario 4b`,` client_secret is `None` - r4b = client.prepare_request_body(username=self.username, password=self.password, client_id=self.client_id, client_secret=None) - self.assertEqual(r4b, 'grant_type=password&username=user_username&password=user_password&client_id=%s' % (self.client_id, )) + r4b = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret=None) + r4b_params = dict(urlparse.parse_qsl(r4b, keep_blank_values=True)) + self.assertEqual(len(r4b_params.keys()), 4) + self.assertEqual(r4b_params['grant_type'], 'password') + self.assertEqual(r4b_params['username'], self.username) + self.assertEqual(r4b_params['password'], self.password) + self.assertEqual(r4b_params['client_id'], self.client_id) diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index fb800f74..3d9c1882 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -16,6 +16,12 @@ from ....unittest import TestCase +# this is the same import method used in oauthlib/oauth2/rfc6749/parameters.py +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + @patch('time.time', new=lambda: 1000) class WebApplicationClientTest(TestCase): @@ -207,21 +213,34 @@ def test_prepare_request_body(self): # scenario 3, Include client_id and client_secret in the body (RFC alternative solution) # the order of kwargs being appended is not guaranteed. for brevity, check the 2 permutations instead of sorting r3 = client.prepare_request_body(client_secret=self.client_secret) - self.assertIn(r3, ('grant_type=authorization_code&client_secret=%s&client_id=%s' % (self.client_secret, self.client_id, ), - 'grant_type=authorization_code&client_id=%s&client_secret=%s' % (self.client_id, self.client_secret, ), - )) + r3_params = dict(urlparse.parse_qsl(r3, keep_blank_values=True)) + self.assertEqual(len(r3_params.keys()), 3) + self.assertEqual(r3_params['grant_type'], 'authorization_code') + self.assertEqual(r3_params['client_id'], self.client_id) + self.assertEqual(r3_params['client_secret'], self.client_secret) + r3b = client.prepare_request_body(include_client_id=True, client_secret=self.client_secret) - self.assertIn(r3b, ('grant_type=authorization_code&client_secret=%s&client_id=%s' % (self.client_secret, self.client_id, ), - 'grant_type=authorization_code&client_id=%s&client_secret=%s' % (self.client_id, self.client_secret, ), - )) + r3b_params = dict(urlparse.parse_qsl(r3b, keep_blank_values=True)) + self.assertEqual(len(r3b_params.keys()), 3) + self.assertEqual(r3b_params['grant_type'], 'authorization_code') + self.assertEqual(r3b_params['client_id'], self.client_id) + self.assertEqual(r3b_params['client_secret'], self.client_secret) # scenario 4, `client_secret` is an empty string r4 = client.prepare_request_body(include_client_id=True, client_secret='') - self.assertEqual(r4, 'grant_type=authorization_code&client_id=%s&client_secret=' % self.client_id) + r4_params = dict(urlparse.parse_qsl(r4, keep_blank_values=True)) + self.assertEqual(len(r4_params.keys()), 3) + self.assertEqual(r4_params['grant_type'], 'authorization_code') + self.assertEqual(r4_params['client_id'], self.client_id) + self.assertEqual(r4_params['client_secret'], '') + # scenario 4b, `client_secret` is `None` r4b = client.prepare_request_body(include_client_id=True, client_secret=None) - self.assertEqual(r4b, 'grant_type=authorization_code&client_id=%s' % self.client_id) + r4b_params = dict(urlparse.parse_qsl(r4b, keep_blank_values=True)) + self.assertEqual(len(r4b_params.keys()), 2) + self.assertEqual(r4b_params['grant_type'], 'authorization_code') + self.assertEqual(r4b_params['client_id'], self.client_id) # scenario Warnings with warnings.catch_warnings(record=True) as w: From 5873be567db7447ac7992357b6fd61c5dc5b4bf1 Mon Sep 17 00:00:00 2001 From: Free Duerinckx Date: Thu, 20 Sep 2018 15:48:12 +0200 Subject: [PATCH 103/237] fixup! `invalid_grant` status code should be 400 --- oauthlib/oauth2/rfc6749/errors.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 482c7404..8c8bda39 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -246,6 +246,8 @@ class InvalidGrantError(OAuth2Error): owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. + + https://tools.ietf.org/html/rfc6749#section-5.2 """ error = 'invalid_grant' status_code = 400 From a77fb1f1a9a9295553d29f20b5cdb6bbeb22cb78 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Thu, 20 Sep 2018 17:56:27 -0400 Subject: [PATCH 104/237] * changed "function definition" to "function signature" in two docstrings * fixed some formatting issues in `prepare_token_request` docstring * slightly altered `prepare_token_request` in handling nontruthy values for `client_secret`. --- .../rfc6749/clients/service_application.py | 2 +- oauthlib/oauth2/rfc6749/parameters.py | 33 +++++++++++-------- .../rfc6749/clients/test_web_application.py | 1 - 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index 759e0d21..35333d81 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -121,7 +121,7 @@ def prepare_request_body(self, :param kwargs: Extra credentials to include in the token request. Parameters marked with a `*` above are not explicit arguments in the - function definition, but are specially documented arguments for items + function signature, but are specially documented arguments for items appearing in the generic `**kwargs` keyworded input. The "scope" parameter may be used, as defined in the Assertion diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 21c8605b..4d0baee7 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -100,13 +100,6 @@ def prepare_token_request(grant_type, body='', include_client_id=True, **kwargs) :param body: Existing request body (URL encoded string) to embed parameters into. This may contain extra paramters. Default ''. - :param code: If using authorization_code grant, pass the previously - obtained authorization code as the ``code`` argument. - - :param redirect_uri: If the "redirect_uri" parameter was included in the - authorization request as described in - `Section 4.1.1`_, and their values MUST be identical. * - :param include_client_id: `True` (default) to send the `client_id` in the body of the upstream request. This is required if the client is not authenticating with the @@ -117,10 +110,22 @@ def prepare_token_request(grant_type, body='', include_client_id=True, **kwargs) :param client_id: Unicode client identifier. Will only appear if `include_client_id` is True. * + :param client_secret: Unicode client secret. Will only appear if set to a + value that is not `None`. Invoking this function with + an empty string will send an empty `client_secret` + value to the server. * + + :param code: If using authorization_code grant, pass the previously + obtained authorization code as the ``code`` argument. * + + :param redirect_uri: If the "redirect_uri" parameter was included in the + authorization request as described in + `Section 4.1.1`_, and their values MUST be identical. * + :param kwargs: Extra arguments to embed in the request body. Parameters marked with a `*` above are not explicit arguments in the - function definition, but are specially documented arguments for items + function signature, but are specially documented arguments for items appearing in the generic `**kwargs` keyworded input. An example of an authorization code token request body: @@ -143,15 +148,17 @@ def prepare_token_request(grant_type, body='', include_client_id=True, **kwargs) if client_id is not None: params.append((unicode_type('client_id'), client_id)) + # the kwargs iteration below only supports including boolean truth (truthy) + # values, but some servers may require an empty string for `client_secret` + client_secret = kwargs.pop('client_secret', None) + if client_secret is not None: + params.append((unicode_type('client_secret'), client_secret)) + + # this handles: `code`, `redirect_uri`, and other undocumented params for k in kwargs: - # this handles: `code`, `redirect_uri`, or undocumented params if kwargs[k]: params.append((unicode_type(k), kwargs[k])) - if ('client_secret' in kwargs) and ('client_secret' not in params): - if kwargs['client_secret'] == '': - params.append((unicode_type('client_secret'), kwargs['client_secret'])) - return add_params_to_qs(body, params) diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 3d9c1882..092f93e7 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -234,7 +234,6 @@ def test_prepare_request_body(self): self.assertEqual(r4_params['client_id'], self.client_id) self.assertEqual(r4_params['client_secret'], '') - # scenario 4b, `client_secret` is `None` r4b = client.prepare_request_body(include_client_id=True, client_secret=None) r4b_params = dict(urlparse.parse_qsl(r4b, keep_blank_values=True)) From 326456cb78eb6b50e6f44f01cb0eaccc7652cf1f Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Thu, 20 Sep 2018 23:10:17 +0100 Subject: [PATCH 105/237] Fix OIDC tests (#565) * Unmute ignored OIDC tests. * Fix more import errors. * Remove recently invalidated test for id_token_hint. * Fix tested grants. * Fix import on py27. --- .../openid/connect/core/endpoints/__init__.py | 0 .../connect/core/endpoints/pre_configured.py | 30 ++++++++++--------- .../connect/core/grant_types/__init__.py | 6 ++-- .../rfc5849/endpoints/test_authorization.py | 2 +- .../openid/connect/core/endpoints/__init__.py | 0 .../core/endpoints/test_claims_handling.py | 7 ++--- .../test_openid_connect_params_handling.py | 8 ++--- .../connect/core/grant_types/__init__.py | 0 .../grant_types/test_authorization_code.py | 10 ++----- .../core/grant_types/test_dispatchers.py | 8 ++--- .../connect/core/grant_types/test_hybrid.py | 3 +- .../connect/core/grant_types/test_implicit.py | 18 +++-------- .../connect/core/test_request_validator.py | 2 +- tests/openid/connect/core/test_server.py | 2 +- tests/openid/connect/core/test_tokens.py | 2 +- 15 files changed, 42 insertions(+), 56 deletions(-) create mode 100644 oauthlib/openid/connect/core/endpoints/__init__.py create mode 100644 tests/openid/connect/core/endpoints/__init__.py create mode 100644 tests/openid/connect/core/grant_types/__init__.py diff --git a/oauthlib/openid/connect/core/endpoints/__init__.py b/oauthlib/openid/connect/core/endpoints/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 3bcd24de..04bd628c 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -8,29 +8,31 @@ """ from __future__ import absolute_import, unicode_literals -from ..grant_types import ( +from oauthlib.oauth2.rfc6749.endpoints import ( + AuthorizationEndpoint, + ResourceEndpoint, + RevocationEndpoint, + TokenEndpoint +) +from oauthlib.oauth2.rfc6749.grant_types import ( AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant, - ClientCredentialsGrant, ImplicitGrant as OAuth2ImplicitGrant, + ClientCredentialsGrant, RefreshTokenGrant, ResourceOwnerPasswordCredentialsGrant ) - -from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant -from oauthlib.openid.connect.core.grant_types.dispatchers import ( +from oauthlib.oauth2.rfc6749.tokens import BearerToken +from ..grant_types import ( + AuthorizationCodeGrant, + ImplicitGrant, + HybridGrant, +) +from ..grant_types.dispatchers import ( AuthorizationCodeGrantDispatcher, ImplicitTokenGrantDispatcher, AuthorizationTokenGrantDispatcher ) -from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant -from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant -from oauthlib.openid.connect.core.tokens import JWTToken - -from ..tokens import BearerToken -from .authorization import AuthorizationEndpoint -from .resource import ResourceEndpoint -from .revocation import RevocationEndpoint -from .token import TokenEndpoint +from ..tokens import JWTToken class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, diff --git a/oauthlib/openid/connect/core/grant_types/__init__.py b/oauthlib/openid/connect/core/grant_types/__init__.py index 7fc183d6..63f30ac9 100644 --- a/oauthlib/openid/connect/core/grant_types/__init__.py +++ b/oauthlib/openid/connect/core/grant_types/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -oauthlib.oauth2.rfc6749.grant_types -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +oauthlib.openid.connect.core.grant_types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ """ from __future__ import unicode_literals, absolute_import @@ -10,7 +10,7 @@ from .base import GrantTypeBase from .hybrid import HybridGrant from .exceptions import OIDCNoPrompt -from oauthlib.openid.connect.core.grant_types.dispatchers import ( +from .dispatchers import ( AuthorizationCodeGrantDispatcher, ImplicitTokenGrantDispatcher, AuthorizationTokenGrantDispatcher diff --git a/tests/oauth1/rfc5849/endpoints/test_authorization.py b/tests/oauth1/rfc5849/endpoints/test_authorization.py index 022e8e9e..e9d3604f 100644 --- a/tests/oauth1/rfc5849/endpoints/test_authorization.py +++ b/tests/oauth1/rfc5849/endpoints/test_authorization.py @@ -6,7 +6,7 @@ from oauthlib.oauth1.rfc5849 import errors from oauthlib.oauth1.rfc5849.endpoints import AuthorizationEndpoint -from ....unittest import TestCase +from tests.unittest import TestCase class AuthorizationEndpointTest(TestCase): diff --git a/tests/openid/connect/core/endpoints/__init__.py b/tests/openid/connect/core/endpoints/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/openid/connect/core/endpoints/test_claims_handling.py b/tests/openid/connect/core/endpoints/test_claims_handling.py index 37a7cdda..d5908a88 100644 --- a/tests/openid/connect/core/endpoints/test_claims_handling.py +++ b/tests/openid/connect/core/endpoints/test_claims_handling.py @@ -11,11 +11,10 @@ import mock from oauthlib.oauth2 import RequestValidator +from oauthlib.openid.connect.core.endpoints.pre_configured import Server -from oauthlib.oauth2.rfc6749.endpoints.pre_configured import Server - -from ....unittest import TestCase -from .test_utils import get_query_credentials +from tests.unittest import TestCase +from tests.oauth2.rfc6749.endpoints.test_utils import get_query_credentials class TestClaimsHandling(TestCase): diff --git a/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py b/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py index 89431b64..517239a5 100644 --- a/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py +++ b/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py @@ -5,10 +5,10 @@ from oauthlib.oauth2 import InvalidRequestError from oauthlib.oauth2.rfc6749.endpoints.authorization import \ AuthorizationEndpoint -from oauthlib.oauth2.rfc6749.grant_types import OpenIDConnectAuthCode from oauthlib.oauth2.rfc6749.tokens import BearerToken +from oauthlib.openid.connect.core.grant_types import AuthorizationCodeGrant -from ....unittest import TestCase +from tests.unittest import TestCase try: from urllib.parse import urlencode @@ -16,14 +16,12 @@ from urllib import urlencode - - class OpenIDConnectEndpointTest(TestCase): def setUp(self): self.mock_validator = mock.MagicMock() self.mock_validator.authenticate_client.side_effect = self.set_client - grant = OpenIDConnectAuthCode(request_validator=self.mock_validator) + grant = AuthorizationCodeGrant(request_validator=self.mock_validator) bearer = BearerToken(self.mock_validator) self.endpoint = AuthorizationEndpoint(grant, bearer, response_types={'code': grant}) diff --git a/tests/openid/connect/core/grant_types/__init__.py b/tests/openid/connect/core/grant_types/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/openid/connect/core/grant_types/test_authorization_code.py b/tests/openid/connect/core/grant_types/test_authorization_code.py index 1bad120f..9bbe7fb8 100644 --- a/tests/openid/connect/core/grant_types/test_authorization_code.py +++ b/tests/openid/connect/core/grant_types/test_authorization_code.py @@ -11,8 +11,9 @@ from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt -from ....unittest import TestCase -from ....oauth2.rfc6749.grant_types.test_authorization_code import AuthorizationCodeGrantTest +from tests.unittest import TestCase +from tests.oauth2.rfc6749.grant_types.test_authorization_code import \ + AuthorizationCodeGrantTest def get_id_token_mock(token, token_handler, request): @@ -81,12 +82,7 @@ def test_no_prompt_authorization(self, generate_token): self.auth.validate_authorization_request, self.request) - # prompt == none requires id token hint bearer = BearerToken(self.mock_validator) - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) self.request.response_mode = 'query' self.request.id_token_hint = 'me@email.com' diff --git a/tests/openid/connect/core/grant_types/test_dispatchers.py b/tests/openid/connect/core/grant_types/test_dispatchers.py index 84f26880..e7dce45f 100644 --- a/tests/openid/connect/core/grant_types/test_dispatchers.py +++ b/tests/openid/connect/core/grant_types/test_dispatchers.py @@ -17,7 +17,7 @@ ) -from ....unittest import TestCase +from tests.unittest import TestCase class ImplicitTokenGrantDispatcherTest(TestCase): @@ -47,12 +47,12 @@ def test_validate_authorization_request_openid(self): def test_create_authorization_response_oauth(self): self.request.scopes = ('hello', 'world') handler = self.dispatcher._handler_for_request(self.request) - self.assertIsInstance(handler, ImplicitGrant) + self.assertIsInstance(handler, OAuth2ImplicitGrant) def test_validate_authorization_request_oauth(self): self.request.scopes = ('hello', 'world') handler = self.dispatcher._handler_for_request(self.request) - self.assertIsInstance(handler, ImplicitGrant) + self.assertIsInstance(handler, OAuth2ImplicitGrant) class DispatcherTest(TestCase): @@ -66,7 +66,7 @@ def setUp(self): self.request_validator = mock.MagicMock() self.auth_grant = OAuth2AuthorizationCodeGrant(self.request_validator) - self.openid_connect_auth = OAuth2AuthorizationCodeGrant(self.request_validator) + self.openid_connect_auth = AuthorizationCodeGrant(self.request_validator) class AuthTokenGrantDispatcherOpenIdTest(DispatcherTest): diff --git a/tests/openid/connect/core/grant_types/test_hybrid.py b/tests/openid/connect/core/grant_types/test_hybrid.py index 531ae7f2..6eb80370 100644 --- a/tests/openid/connect/core/grant_types/test_hybrid.py +++ b/tests/openid/connect/core/grant_types/test_hybrid.py @@ -2,7 +2,8 @@ from __future__ import absolute_import, unicode_literals from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant -from ....oauth2.rfc6749.grant_types.test_authorization_code import AuthorizationCodeGrantTest +from tests.oauth2.rfc6749.grant_types.test_authorization_code import \ + AuthorizationCodeGrantTest class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py index 56247d99..c369bb60 100644 --- a/tests/openid/connect/core/grant_types/test_implicit.py +++ b/tests/openid/connect/core/grant_types/test_implicit.py @@ -4,18 +4,14 @@ import mock from oauthlib.common import Request - from oauthlib.oauth2.rfc6749.tokens import BearerToken - -from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant -from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt - -from ....unittest import TestCase +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant +from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant +from tests.oauth2.rfc6749.grant_types.test_implicit import ImplicitGrantTest +from tests.unittest import TestCase from .test_authorization_code import get_id_token_mock, OpenIDAuthCodeTest -from ....oauth2.rfc6749.grant_types.test_implicit import ImplicitGrantTest - class OpenIDImplicitInterferenceTest(ImplicitGrantTest): """Test that OpenID don't interfere with normal OAuth 2 flows.""" @@ -80,13 +76,7 @@ def test_no_prompt_authorization(self, generate_token): self.auth.validate_authorization_request, self.request) - # prompt == none requires id token hint bearer = BearerToken(self.mock_validator) - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) - self.request.id_token_hint = 'me@email.com' h, b, s = self.auth.create_authorization_response(self.request, bearer) self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) diff --git a/tests/openid/connect/core/test_request_validator.py b/tests/openid/connect/core/test_request_validator.py index 14a7c232..1e71fb16 100644 --- a/tests/openid/connect/core/test_request_validator.py +++ b/tests/openid/connect/core/test_request_validator.py @@ -3,7 +3,7 @@ from oauthlib.openid.connect.core.request_validator import RequestValidator -from ....unittest import TestCase +from tests.unittest import TestCase class RequestValidatorTest(TestCase): diff --git a/tests/openid/connect/core/test_server.py b/tests/openid/connect/core/test_server.py index 83290db8..a83f22df 100644 --- a/tests/openid/connect/core/test_server.py +++ b/tests/openid/connect/core/test_server.py @@ -14,7 +14,7 @@ from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant -from ....unittest import TestCase +from tests.unittest import TestCase class AuthorizationEndpointTest(TestCase): diff --git a/tests/openid/connect/core/test_tokens.py b/tests/openid/connect/core/test_tokens.py index 12c75f10..1fcfb517 100644 --- a/tests/openid/connect/core/test_tokens.py +++ b/tests/openid/connect/core/test_tokens.py @@ -4,7 +4,7 @@ from oauthlib.openid.connect.core.tokens import JWTToken -from ....unittest import TestCase +from tests.unittest import TestCase class JWTTokenTestCase(TestCase): From 320a209a0ba03b2d5ca815621a305360a8271955 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Sun, 23 Sep 2018 21:56:11 +0100 Subject: [PATCH 106/237] Tidy up templates a bit. --- .github/ISSUE_TEMPLATE/bug_report.md | 29 +++++++++++++---------- .github/ISSUE_TEMPLATE/feature_request.md | 14 +++++------ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 9c82293a..4c5a84bf 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,21 +1,24 @@ --- name: Bug report about: Create a report to help us improve - --- - **Describe the bug** -A clear and concise description of what the bug is. -**To Reproduce** -Steps to reproduce the behavior +A clear and concise description of what the problem is. + +**How to reproduce** + +Steps to reproduce the behavior. **Expected behavior** -A clear and concise description of what you expected to happen. - -**Context** -Are you using OAuth1 ? OAuth2 ? -Using a client ? a public provider ? -Implementing your own provider ? -Using a downstream library ? (requests-oauthlib, django-oauth-toolkit ...) -Add any other context. + +A description of what you expected to happen. + +**Additional context** + +Please provide any further context here. + +- Are you using OAuth1, OAuth2 or OIDC? +- Are you writing client or server side code? +- If client, what provider are you connecting to? +- Are you using a downstream library, such as `requests-oauthlib`, `django-oauth-toolkit`, ...? diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 81e6b068..a415f6c5 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,14 +1,14 @@ --- name: Feature request about: Suggest an idea for this project - --- +**Describe the feature** -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. +A clear and concise description of what you would like to see. **Additional context** -Add any other context or screenshots about the feature request here. + +Please provide any further context here. + +- Does the feature apply to OAuth1, OAuth2 and/or OIDC? +- Does the feature apply to client or server side code? From 59a0cb748ad66d7b3a59848bae22e188fd3eec5c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 5 Oct 2018 11:32:39 +0200 Subject: [PATCH 107/237] Bump version to prepare 3.0.0 --- oauthlib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index bc5d96b5..5b1b3809 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,6 +12,6 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '2.1.0' +__version__ = '3.0.0-dev' logging.getLogger('oauthlib').addHandler(NullHandler()) From 49091c97f765c20a645fa450a3fc824267e45613 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 25 Oct 2018 12:03:40 +0200 Subject: [PATCH 108/237] Initial OAuth Authorization Server Metadata RFC8414 --- docs/feature_matrix.rst | 1 + oauthlib/oauth2/__init__.py | 1 + oauthlib/oauth2/rfc6749/endpoints/__init__.py | 1 + oauthlib/oauth2/rfc6749/endpoints/metadata.py | 191 ++++++++++++++++++ .../oauth2/rfc6749/endpoints/test_metadata.py | 28 +++ 5 files changed, 222 insertions(+) create mode 100644 oauthlib/oauth2/rfc6749/endpoints/metadata.py create mode 100644 tests/oauth2/rfc6749/endpoints/test_metadata.py diff --git a/docs/feature_matrix.rst b/docs/feature_matrix.rst index 59f3f3ab..672cc27e 100644 --- a/docs/feature_matrix.rst +++ b/docs/feature_matrix.rst @@ -18,6 +18,7 @@ OAuth 2 client and provider support for - Draft MAC tokens - Token Revocation - Token Introspection +- OAuth Authorization Server Metadata - OpenID Connect Authentication with support for SAML2 and JWT tokens, dynamic client registration and more to diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index 303c6a1d..3f437556 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -16,6 +16,7 @@ from .rfc6749.clients import ServiceApplicationClient from .rfc6749.endpoints import AuthorizationEndpoint from .rfc6749.endpoints import IntrospectEndpoint +from .rfc6749.endpoints import MetadataEndpoint from .rfc6749.endpoints import TokenEndpoint from .rfc6749.endpoints import ResourceEndpoint from .rfc6749.endpoints import RevocationEndpoint diff --git a/oauthlib/oauth2/rfc6749/endpoints/__init__.py b/oauthlib/oauth2/rfc6749/endpoints/__init__.py index 9557f92a..51e173df 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/__init__.py +++ b/oauthlib/oauth2/rfc6749/endpoints/__init__.py @@ -10,6 +10,7 @@ from .authorization import AuthorizationEndpoint from .introspect import IntrospectEndpoint +from .metadata import MetadataEndpoint from .token import TokenEndpoint from .resource import ResourceEndpoint from .revocation import RevocationEndpoint diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py new file mode 100644 index 00000000..8c7699b4 --- /dev/null +++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749.endpoint.metadata +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An implementation of the `OAuth 2.0 Authorization Server Metadata`. + +.. _`OAuth 2.0 Authorization Server Metadata`: https://tools.ietf.org/html/rfc8414 +""" +from __future__ import absolute_import, unicode_literals + +import copy +import json +import logging + +from ....common import unicode_type +from .base import BaseEndpoint, catch_errors_and_unavailability +from .authorization import AuthorizationEndpoint +from .introspect import IntrospectEndpoint +from .token import TokenEndpoint +from .revocation import RevocationEndpoint + + +log = logging.getLogger(__name__) + + +class MetadataEndpoint(BaseEndpoint): + + """OAuth2.0 Authorization Server Metadata endpoint. + + This specification generalizes the metadata format defined by + `OpenID Connect Discovery 1.0` in a way that is compatible + with OpenID Connect Discovery while being applicable to a wider set + of OAuth 2.0 use cases. This is intentionally parallel to the way + that `OAuth 2.0 Dynamic Client Registration Protocol` [RFC7591] + generalized the dynamic client registration mechanisms defined by + `OpenID Connect Dynamic Client Registration 1.0` + in a way that is compatible with it. + + .. _`OpenID Connect Discovery 1.0`: http://openid.net/specs/openid-connect-discovery-1_0.html + .. _`OAuth 2.0 Dynamic Client Registration Protocol`: https://tools.ietf.org/html/rfc7591 + .. _`OpenID Connect Dynamic Client Registration 1.0`: https://openid.net/specs/openid-connect-registration-1_0.html + """ + + def __init__(self, endpoints, claims={}, raise_errors=True): + assert isinstance(claims, dict) + for endpoint in endpoints: + assert isinstance(endpoint, BaseEndpoint) + + BaseEndpoint.__init__(self) + self.raise_errors = raise_errors + self.endpoints = endpoints + self.initial_claims = claims + self.claims = self.validate_metadata_server() + + @catch_errors_and_unavailability + def create_metadata_response(self, uri, http_method='GET', body=None, + headers=None): + """Create metadata response + """ + headers = { + 'Content-Type': 'application/json' + } + return headers, json.dumps(self.claims), 200 + + def validate_metadata(self, array, key, is_required=False, is_list=False, is_url=False, is_issuer=False): + if not self.raise_errors: + return + + if key not in array: + if is_required: + raise ValueError("key {} is a mandatory metadata.".format(key)) + + elif is_issuer: + if not array[key].startswith("https"): + raise ValueError("key {}: {} must be an HTTPS URL".format(key, array[key])) + if "?" in array[key] or "&" in array[key] or "#" in array[key]: + raise ValueError("key {}: {} must not contain query or fragment components".format(key, array[key])) + + elif is_url: + if not array[key].startswith("http"): + raise ValueError("key {}: {} must be an URL".format(key, array[key])) + + elif is_list: + if not isinstance(array[key], list): + raise ValueError("key {}: {} must be an Array".format(key, array[key])) + for elem in array[key]: + if not isinstance(elem, unicode_type): + raise ValueError("array {}: {} must contains only string (not {})".format(key, array[key], elem)) + + def validate_metadata_token(self, claims, endpoint): + claims["grant_types_supported"] = list(endpoint._grant_types.keys()) + claims["token_endpoint_auth_methods_supported"] = ["client_secret_post", "client_secret_basic"] + + self.validate_metadata(claims, "grant_types_supported", is_list=True) + self.validate_metadata(claims, "token_endpoint_auth_methods_supported", is_list=True) + self.validate_metadata(claims, "token_endpoint_auth_signing_alg_values_supported", is_list=True) + self.validate_metadata(claims, "token_endpoint", is_required=True, is_url=True) + + def validate_metadata_authorization(self, claims, endpoint): + claims["response_types_supported"] = list(self._response_types.keys()) + claims["response_modes_supported"] = ["query", "fragment"] + + self.validate_metadata(claims, "response_types_supported", is_required=True, is_list=True) + self.validate_metadata(claims, "response_modes_supported", is_list=True) + if "code" in claims["response_types_supported"]: + self.validate_metadata(claims, "code_challenge_methods_supported", is_list=True) + self.validate_metadata(claims, "authorization_endpoint", is_required=True, is_url=True) + + def validate_metadata_revocation(self, claims, endpoint): + claims["revocation_endpoint_auth_methods_supported"] = ["client_secret_post", "client_secret_basic"] + + self.validate_metadata(claims, "revocation_endpoint_auth_methods_supported", is_list=True) + self.validate_metadata(claims, "revocation_endpoint_auth_signing_alg_values_supported", is_list=True) + self.validate_metadata(claims, "revocation_endpoint", is_required=True, is_url=True) + + def validate_metadata_introspection(self, claims, endpoint): + claims["introspection_endpoint_auth_methods_supported"] = ["client_secret_post", "client_secret_basic"] + + self.validate_metadata(claims, "introspection_endpoint_auth_methods_supported", is_list=True) + self.validate_metadata(claims, "introspection_endpoint_auth_signing_alg_values_supported", is_list=True) + self.validate_metadata(claims, "introspection_endpoint", is_required=True, is_url=True) + + def validate_metadata_server(self): + """ + Authorization servers can have metadata describing their + configuration. The following authorization server metadata values + are used by this specification. More details can be found in `RFC8414` : + + issuer + REQUIRED + + authorization_endpoint + URL of the authorization server's authorization endpoint + [RFC6749]. This is REQUIRED unless no grant types are supported + that use the authorization endpoint. + + token_endpoint + URL of the authorization server's token endpoint [RFC6749]. This + is REQUIRED unless only the implicit grant type is supported. + + scopes_supported + RECOMMENDED. + + response_types_supported + REQUIRED. + + * Other OPTIONAL fields: + jwks_uri + registration_endpoint + response_modes_supported + grant_types_supported + token_endpoint_auth_methods_supported + token_endpoint_auth_signing_alg_values_supported + service_documentation + ui_locales_supported + op_policy_uri + op_tos_uri + revocation_endpoint + revocation_endpoint_auth_methods_supported + revocation_endpoint_auth_signing_alg_values_supported + introspection_endpoint + introspection_endpoint_auth_methods_supported + introspection_endpoint_auth_signing_alg_values_supported + code_challenge_methods_supported + + Additional authorization server metadata parameters MAY also be used. + Some are defined by other specifications, such as OpenID Connect + Discovery 1.0 [OpenID.Discovery]. + + .. _`RFC8414 section 2`: https://tools.ietf.org/html/rfc8414#section-2 + """ + claims = copy.deepcopy(self.initial_claims) + self.validate_metadata(claims, "issuer", is_required=True, is_issuer=True) + self.validate_metadata(claims, "jwks_uri", is_url=True) + self.validate_metadata(claims, "scopes_supported", is_list=True) + self.validate_metadata(claims, "service_documentation", is_url=True) + self.validate_metadata(claims, "ui_locales_supported", is_list=True) + self.validate_metadata(claims, "op_policy_uri", is_url=True) + self.validate_metadata(claims, "op_tos_uri", is_url=True) + + for endpoint in self.endpoints: + if isinstance(endpoint, TokenEndpoint): + self.validate_metadata_token(claims, endpoint) + if isinstance(endpoint, AuthorizationEndpoint): + self.validate_metadata_authorization(claims, endpoint) + if isinstance(endpoint, RevocationEndpoint): + self.validate_metadata_revocation(claims, endpoint) + if isinstance(endpoint, IntrospectEndpoint): + self.validate_metadata_introspection(claims, endpoint) + return claims diff --git a/tests/oauth2/rfc6749/endpoints/test_metadata.py b/tests/oauth2/rfc6749/endpoints/test_metadata.py new file mode 100644 index 00000000..a07ba630 --- /dev/null +++ b/tests/oauth2/rfc6749/endpoints/test_metadata.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +from oauthlib.oauth2 import MetadataEndpoint +from oauthlib.oauth2 import TokenEndpoint + +from ....unittest import TestCase + + +class MetadataEndpointTest(TestCase): + def setUp(self): + self.metadata = { + "issuer": 'https://foo.bar' + } + + def test_token_endpoint(self): + endpoint = TokenEndpoint(None, None, grant_types={"password": None}) + metadata = MetadataEndpoint([endpoint], { + "issuer": 'https://foo.bar', + "token_endpoint": "https://foo.bar/token" + }) + self.assertIn("grant_types_supported", metadata.claims) + self.assertEqual(metadata.claims["grant_types_supported"], ["password"]) + + def test_mandatory_fields(self): + metadata = MetadataEndpoint([], self.metadata) + self.assertIn("issuer", metadata.claims) + self.assertEqual(metadata.claims["issuer"], 'https://foo.bar') From 15e4f63504c93de7659e26336e95cff61859af11 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 1 Nov 2018 14:03:41 +0100 Subject: [PATCH 109/237] Wrong Client is also a FatalClientError (#608) FatalClientError is it SHOULD NOT be redirected to client (redirect_uri), but MUST be redirected to USERS (error_uri). --- oauthlib/oauth2/rfc6749/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 8c8bda39..7ead3d4c 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -224,7 +224,7 @@ class TemporarilyUnavailableError(OAuth2Error): error = 'temporarily_unavailable' -class InvalidClientError(OAuth2Error): +class InvalidClientError(FatalClientError): """ Client authentication failed (e.g. unknown client, no client authentication included, or unsupported authentication method). From 93b47c7fdb531a463ea4a5f43d36d9ffc8e1aec1 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 20 Nov 2018 10:20:59 +0100 Subject: [PATCH 110/237] Import OIDC main classes identically than OAuth2 import oauthlib.oauth2.Server must be replaced with oauthlib.openid.Server --- oauthlib/openid/__init__.py | 9 +++++++++ oauthlib/openid/connect/core/endpoints/__init__.py | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/oauthlib/openid/__init__.py b/oauthlib/openid/__init__.py index e69de29b..03f0fa2e 100644 --- a/oauthlib/openid/__init__.py +++ b/oauthlib/openid/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.openid +~~~~~~~~~~~~~~ + +""" +from __future__ import absolute_import, unicode_literals + +from .connect.core.endpoints import Server diff --git a/oauthlib/openid/connect/core/endpoints/__init__.py b/oauthlib/openid/connect/core/endpoints/__init__.py index e69de29b..719f883c 100644 --- a/oauthlib/openid/connect/core/endpoints/__init__.py +++ b/oauthlib/openid/connect/core/endpoints/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oopenid.core +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming and providing OpenID Connect +""" +from __future__ import absolute_import, unicode_literals + +from .pre_configured import Server From 2a2e48a67105d99c8faad804650cf7a5c47a4ec4 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 20 Nov 2018 10:23:54 +0100 Subject: [PATCH 111/237] Replaced distinct classes by a more unified one. "default_grant" and "oidc_grant" must be two generic attributes of OpenID Connect Dispatcher. We should not leave each Dispatcher implementation have this own attributes names. --- .../connect/core/endpoints/pre_configured.py | 6 +-- .../connect/core/grant_types/dispatchers.py | 49 ++++++++++--------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 04bd628c..9cf30db4 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -72,8 +72,8 @@ def __init__(self, request_validator, token_expires_in=None, jwt = JWTToken(request_validator, token_generator, token_expires_in, refresh_token_generator) - auth_grant_choice = AuthorizationCodeGrantDispatcher(default_auth_grant=auth_grant, oidc_auth_grant=openid_connect_auth) - implicit_grant_choice = ImplicitTokenGrantDispatcher(default_implicit_grant=implicit_grant, oidc_implicit_grant=openid_connect_implicit) + auth_grant_choice = AuthorizationCodeGrantDispatcher(default_grant=auth_grant, oidc_grant=openid_connect_auth) + implicit_grant_choice = ImplicitTokenGrantDispatcher(default_grant=implicit_grant, oidc_grant=openid_connect_implicit) # See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations # internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination @@ -90,7 +90,7 @@ def __init__(self, request_validator, token_expires_in=None, }, default_token_type=bearer) - token_grant_choice = AuthorizationTokenGrantDispatcher(request_validator, default_token_grant=auth_grant, oidc_token_grant=openid_connect_auth) + token_grant_choice = AuthorizationTokenGrantDispatcher(request_validator, default_grant=auth_grant, oidc_grant=openid_connect_auth) TokenEndpoint.__init__(self, default_grant_type='authorization_code', grant_types={ diff --git a/oauthlib/openid/connect/core/grant_types/dispatchers.py b/oauthlib/openid/connect/core/grant_types/dispatchers.py index 2c334063..be8e2f3c 100644 --- a/oauthlib/openid/connect/core/grant_types/dispatchers.py +++ b/oauthlib/openid/connect/core/grant_types/dispatchers.py @@ -2,20 +2,25 @@ log = logging.getLogger(__name__) -class AuthorizationCodeGrantDispatcher(object): +class Dispatcher(object): + default_grant = None + oidc_grant = None + + +class AuthorizationCodeGrantDispatcher(Dispatcher): """ This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope - including 'openid' to either the default_auth_grant or the oidc_auth_grant based on the scopes requested. + including 'openid' to either the default_grant or the oidc_grant based on the scopes requested. """ - def __init__(self, default_auth_grant=None, oidc_auth_grant=None): - self.default_auth_grant = default_auth_grant - self.oidc_auth_grant = oidc_auth_grant + def __init__(self, default_grant=None, oidc_grant=None): + self.default_grant = default_grant + self.oidc_grant = oidc_grant def _handler_for_request(self, request): - handler = self.default_auth_grant + handler = self.default_grant if request.scopes and "openid" in request.scopes: - handler = self.oidc_auth_grant + handler = self.oidc_grant log.debug('Selecting handler for request %r.', handler) return handler @@ -27,20 +32,20 @@ def validate_authorization_request(self, request): return self._handler_for_request(request).validate_authorization_request(request) -class ImplicitTokenGrantDispatcher(object): +class ImplicitTokenGrantDispatcher(Dispatcher): """ This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope - including 'openid' to either the default_auth_grant or the oidc_auth_grant based on the scopes requested. + including 'openid' to either the default_grant or the oidc_grant based on the scopes requested. """ - def __init__(self, default_implicit_grant=None, oidc_implicit_grant=None): - self.default_implicit_grant = default_implicit_grant - self.oidc_implicit_grant = oidc_implicit_grant + def __init__(self, default_grant=None, oidc_grant=None): + self.default_grant = default_grant + self.oidc_grant = oidc_grant def _handler_for_request(self, request): - handler = self.default_implicit_grant + handler = self.default_grant if request.scopes and "openid" in request.scopes and 'id_token' in request.response_type: - handler = self.oidc_implicit_grant + handler = self.oidc_grant log.debug('Selecting handler for request %r.', handler) return handler @@ -52,31 +57,31 @@ def validate_authorization_request(self, request): return self._handler_for_request(request).validate_authorization_request(request) -class AuthorizationTokenGrantDispatcher(object): +class AuthorizationTokenGrantDispatcher(Dispatcher): """ This is an adapter class that will route simple Token requests, those that authorization_code have a scope - including 'openid' to either the default_token_grant or the oidc_token_grant based on the scopes requested. + including 'openid' to either the default_grant or the oidc_grant based on the scopes requested. """ - def __init__(self, request_validator, default_token_grant=None, oidc_token_grant=None): - self.default_token_grant = default_token_grant - self.oidc_token_grant = oidc_token_grant + def __init__(self, request_validator, default_grant=None, oidc_grant=None): + self.default_grant = default_grant + self.oidc_grant = oidc_grant self.request_validator = request_validator def _handler_for_request(self, request): - handler = self.default_token_grant + handler = self.default_grant scopes = () parameters = dict(request.decoded_body) client_id = parameters.get('client_id', None) code = parameters.get('code', None) redirect_uri = parameters.get('redirect_uri', None) - # If code is not pressent fallback to `default_token_grant` wich will + # If code is not pressent fallback to `default_grant` wich will # raise an error for the missing `code` in `create_token_response` step. if code: scopes = self.request_validator.get_authorization_code_scopes(client_id, code, redirect_uri, request) if 'openid' in scopes: - handler = self.oidc_token_grant + handler = self.oidc_grant log.debug('Selecting handler for request %r.', handler) return handler From 10acc015b7f9a5e166fa3a9afeed8c1b531fa026 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 20 Nov 2018 10:29:16 +0100 Subject: [PATCH 112/237] Fix unit tests for new Dispatch attributes names --- .../connect/core/grant_types/test_dispatchers.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/openid/connect/core/grant_types/test_dispatchers.py b/tests/openid/connect/core/grant_types/test_dispatchers.py index e7dce45f..9e45d656 100644 --- a/tests/openid/connect/core/grant_types/test_dispatchers.py +++ b/tests/openid/connect/core/grant_types/test_dispatchers.py @@ -28,8 +28,8 @@ def setUp(self): openid_connect_implicit = ImplicitGrant(request_validator) self.dispatcher = ImplicitTokenGrantDispatcher( - default_implicit_grant=implicit_grant, - oidc_implicit_grant=openid_connect_implicit + default_grant=implicit_grant, + oidc_grant=openid_connect_implicit ) def test_create_authorization_response_openid(self): @@ -76,8 +76,8 @@ def setUp(self): self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid') self.dispatcher = AuthorizationTokenGrantDispatcher( self.request_validator, - default_token_grant=self.auth_grant, - oidc_token_grant=self.openid_connect_auth + default_grant=self.auth_grant, + oidc_grant=self.openid_connect_auth ) def test_create_token_response_openid(self): @@ -98,8 +98,8 @@ def setUp(self): self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid') self.dispatcher = AuthorizationTokenGrantDispatcher( self.request_validator, - default_token_grant=self.auth_grant, - oidc_token_grant=self.openid_connect_auth + default_grant=self.auth_grant, + oidc_grant=self.openid_connect_auth ) def test_create_token_response_openid_without_code(self): @@ -115,8 +115,8 @@ def setUp(self): self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'world') self.dispatcher = AuthorizationTokenGrantDispatcher( self.request_validator, - default_token_grant=self.auth_grant, - oidc_token_grant=self.openid_connect_auth + default_grant=self.auth_grant, + oidc_grant=self.openid_connect_auth ) def test_create_token_response_oauth(self): From e77023ca5db633c5200322cc5ab8f75b22aa7832 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 21 Nov 2018 15:17:38 +0100 Subject: [PATCH 113/237] Allow custom provider to override oauthlib values See https://github.com/oauthlib/oauthlib/pull/605#discussion_r234438151 --- oauthlib/oauth2/rfc6749/endpoints/metadata.py | 14 ++++++++------ tests/oauth2/rfc6749/endpoints/test_metadata.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py index 8c7699b4..6d77b9f5 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/metadata.py +++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py @@ -89,8 +89,8 @@ def validate_metadata(self, array, key, is_required=False, is_list=False, is_url raise ValueError("array {}: {} must contains only string (not {})".format(key, array[key], elem)) def validate_metadata_token(self, claims, endpoint): - claims["grant_types_supported"] = list(endpoint._grant_types.keys()) - claims["token_endpoint_auth_methods_supported"] = ["client_secret_post", "client_secret_basic"] + claims.setdefault("grant_types_supported", list(endpoint._grant_types.keys())) + claims.setdefault("token_endpoint_auth_methods_supported", ["client_secret_post", "client_secret_basic"]) self.validate_metadata(claims, "grant_types_supported", is_list=True) self.validate_metadata(claims, "token_endpoint_auth_methods_supported", is_list=True) @@ -98,8 +98,8 @@ def validate_metadata_token(self, claims, endpoint): self.validate_metadata(claims, "token_endpoint", is_required=True, is_url=True) def validate_metadata_authorization(self, claims, endpoint): - claims["response_types_supported"] = list(self._response_types.keys()) - claims["response_modes_supported"] = ["query", "fragment"] + claims.setdefault("response_types_supported", list(self._response_types.keys())) + claims.setdefault("response_modes_supported", ["query", "fragment"]) self.validate_metadata(claims, "response_types_supported", is_required=True, is_list=True) self.validate_metadata(claims, "response_modes_supported", is_list=True) @@ -108,14 +108,16 @@ def validate_metadata_authorization(self, claims, endpoint): self.validate_metadata(claims, "authorization_endpoint", is_required=True, is_url=True) def validate_metadata_revocation(self, claims, endpoint): - claims["revocation_endpoint_auth_methods_supported"] = ["client_secret_post", "client_secret_basic"] + claims.setdefault("revocation_endpoint_auth_methods_supported", + ["client_secret_post", "client_secret_basic"]) self.validate_metadata(claims, "revocation_endpoint_auth_methods_supported", is_list=True) self.validate_metadata(claims, "revocation_endpoint_auth_signing_alg_values_supported", is_list=True) self.validate_metadata(claims, "revocation_endpoint", is_required=True, is_url=True) def validate_metadata_introspection(self, claims, endpoint): - claims["introspection_endpoint_auth_methods_supported"] = ["client_secret_post", "client_secret_basic"] + claims.setdefault("introspection_endpoint_auth_methods_supported", + ["client_secret_post", "client_secret_basic"]) self.validate_metadata(claims, "introspection_endpoint_auth_methods_supported", is_list=True) self.validate_metadata(claims, "introspection_endpoint_auth_signing_alg_values_supported", is_list=True) diff --git a/tests/oauth2/rfc6749/endpoints/test_metadata.py b/tests/oauth2/rfc6749/endpoints/test_metadata.py index a07ba630..301e8469 100644 --- a/tests/oauth2/rfc6749/endpoints/test_metadata.py +++ b/tests/oauth2/rfc6749/endpoints/test_metadata.py @@ -22,6 +22,16 @@ def test_token_endpoint(self): self.assertIn("grant_types_supported", metadata.claims) self.assertEqual(metadata.claims["grant_types_supported"], ["password"]) + def test_token_endpoint_overridden(self): + endpoint = TokenEndpoint(None, None, grant_types={"password": None}) + metadata = MetadataEndpoint([endpoint], { + "issuer": 'https://foo.bar', + "token_endpoint": "https://foo.bar/token", + "grant_types_supported": ["pass_word_special_provider"] + }) + self.assertIn("grant_types_supported", metadata.claims) + self.assertEqual(metadata.claims["grant_types_supported"], ["pass_word_special_provider"]) + def test_mandatory_fields(self): metadata = MetadataEndpoint([], self.metadata) self.assertIn("issuer", metadata.claims) From fb23d864aa55b74f678ee7e9efe2ea5f938d63d8 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 23 Nov 2018 09:11:27 +0100 Subject: [PATCH 114/237] Add OIDC and id_token as JWT example --- docs/feature_matrix.rst | 51 ++++++++++++++++++++--------- docs/oauth2/endpoints/endpoints.rst | 2 +- docs/oauth2/oidc/id_tokens.rst | 35 +++++++++++++++++++- docs/oauth2/oidc/validator.rst | 29 +++++++++++++--- 4 files changed, 96 insertions(+), 21 deletions(-) diff --git a/docs/feature_matrix.rst b/docs/feature_matrix.rst index 59f3f3ab..45010d15 100644 --- a/docs/feature_matrix.rst +++ b/docs/feature_matrix.rst @@ -7,21 +7,31 @@ Extensions and variations that are outside the spec are not supported. - HMAC-SHA1, RSA-SHA1 and plaintext signatures. - Signature placement in header, url or body. -OAuth 2 client and provider support for - -- Authorization Code Grant -- Implicit Grant -- Client Credentials Grant -- Resource Owner Password Credentials Grant -- Refresh Tokens -- Bearer Tokens -- Draft MAC tokens -- Token Revocation -- Token Introspection -- OpenID Connect Authentication - -with support for SAML2 and JWT tokens, dynamic client registration and more to -come. +OAuth 2.0 client and provider support for: + +- `RFC6749#section-4.1`_: Authorization Code Grant +- `RFC6749#section-4.2`_: Implicit Grant +- `RFC6749#section-4.3`_: Resource Owner Password Credentials Grant +- `RFC6749#section-4.4`_: Client Credentials Grant +- `RFC6749#section-6`_: Refresh Tokens +- `RFC6750`_: Bearer Tokens +- `RFC7009`_: Token Revocation +- `RFC Draft MAC tokens`_ +- OAuth2.0 Provider: `OpenID Connect Core`_ +- OAuth2.0 Provider: `RFC7662`_: Token Introspection +- OAuth2.0 Provider: `RFC8414`_: Authorization Server Metadata + +Features to be implemented (any help/PR are welcomed): + +- OAuth2.0 Client: `OpenID Connect Core`_ +- OAuth2.0 Client: `RFC7662`_: Token Introspection +- OAuth2.0 Client: `RFC8414`_: Authorization Server Metadata +- SAML2 +- Bearer JWT as Client Authentication +- Dynamic client registration +- OpenID Discovery +- OpenID Session Management +- ...and more Supported platforms ------------------- @@ -32,3 +42,14 @@ should be able to use OAuthLib on any platform that supports Python. If you use RSA you are limited to the platforms supported by `cryptography`_. .. _`cryptography`: https://cryptography.io/en/latest/installation/ +.. _`RFC6749#section-4.1`: https://tools.ietf.org/html/rfc6749#section-4.1 +.. _`RFC6749#section-4.2`: https://tools.ietf.org/html/rfc6749#section-4.2 +.. _`RFC6749#section-4.3`: https://tools.ietf.org/html/rfc6749#section-4.3 +.. _`RFC6749#section-4.4`: https://tools.ietf.org/html/rfc6749#section-4.4 +.. _`RFC6749#section-6`: https://tools.ietf.org/html/rfc6749#section-6 +.. _`RFC6750`: https://tools.ietf.org/html/rfc6750 +.. _`RFC Draft MAC tokens`: https://tools.ietf.org/id/draft-ietf-oauth-v2-http-mac-02.html +.. _`RFC7009`: https://tools.ietf.org/html/rfc7009 +.. _`RFC7662`: https://tools.ietf.org/html/rfc7662 +.. _`OpenID Connect Core`: https://openid.net/specs/openid-connect-core-1_0.html +.. _`RFC8414`: https://tools.ietf.org/html/rfc8414 diff --git a/docs/oauth2/endpoints/endpoints.rst b/docs/oauth2/endpoints/endpoints.rst index 98599e87..8068ec46 100644 --- a/docs/oauth2/endpoints/endpoints.rst +++ b/docs/oauth2/endpoints/endpoints.rst @@ -16,8 +16,8 @@ client attempts to access the user resources on their behalf. authorization introspect token - resource revocation + resource There are three main endpoints, the authorization endpoint which mainly handles user authorization, the token endpoint which provides tokens and the diff --git a/docs/oauth2/oidc/id_tokens.rst b/docs/oauth2/oidc/id_tokens.rst index 5d6aa917..999cfa7e 100644 --- a/docs/oauth2/oidc/id_tokens.rst +++ b/docs/oauth2/oidc/id_tokens.rst @@ -5,7 +5,9 @@ The creation of `ID Tokens`_ is ultimately done not by OAuthLib but by your ``Re content is dependent on your implementation of users, their attributes, any claims you may wish to support, as well as the details of how you model the notion of a Client Application. As such OAuthLib simply calls your validator's ``get_id_token`` method at the appropriate times during the authorization flow, depending on the grant type requested (Authorization Code, Implicit, -Hybrid, etc.) +Hybrid, etc.). + +See examples below. .. _`ID Tokens`: http://openid.net/specs/openid-connect-core-1_0.html#IDToken @@ -13,4 +15,35 @@ Hybrid, etc.) :members: get_id_token +JWT/JWS example with pyjwt library +---------------------------------- + +An example below using Cryptography library to load the private key and PyJWT to sign the JWT. +Note that the claims list in the "data" dict must be set accordingly to the auth request. + +You can switch to jwcrypto library if you want to return JWE instead. + +.. code-block:: python + + class MyValidator(RequestValidator): + def __init__(self, **kwargs): + with open(path.join(path.dirname(path.realpath(__file__)), "./id_rsa"), 'rb') as fd: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + self.private_pem = serialization.load_pem_private_key( + fd.read(), + password=None, + backend=default_backend() + ) + + super().__init__(self, **kwargs) + + def get_id_token(self, token, token_handler, request): + import jwt + + data = {"nonce": request.nonce} if request.nonce is not None else {} + + for claim_key in request.claims: + data[claim_key] = request.userattributes[claim_key] # this must be set in another callback + return jwt.encode(data, self.private_pem, 'RS256') diff --git a/docs/oauth2/oidc/validator.rst b/docs/oauth2/oidc/validator.rst index c92b726a..a03adfe4 100644 --- a/docs/oauth2/oidc/validator.rst +++ b/docs/oauth2/oidc/validator.rst @@ -1,7 +1,28 @@ -RequestValidator Extensions -============================ +OpenID Connect +========================================= -Four methods must be implemented in your validator subclass if you wish to support OpenID Connect: +Migrate your OAuth2.0 server into an OIDC provider +---------------------------------------------------- + +If you have a OAuth2.0 provider running and want to upgrade to OIDC, you can +upgrade it by replacing one line of code: + +.. code-block:: python + + from oauthlib.oauth2 import Server + +Into + +.. code-block:: python + + from oauthlib.openid import Server + +Then, you have to implement the new RequestValidator methods as shown below. + +RequestValidator Extension +---------------------------------------------------- + +A couple of methods must be implemented in your validator subclass if you wish to support OpenID Connect: .. autoclass:: oauthlib.oauth2.RequestValidator - :members: validate_silent_authorization, validate_silent_login, validate_user_match, get_id_token + :members: validate_silent_authorization, validate_silent_login, validate_user_match, get_id_token, get_authorization_code_scopes, validate_jwt_bearer_token From 01b3c4c20178b292d470eead153b91feaa05c057 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 29 Nov 2018 16:43:00 +0100 Subject: [PATCH 115/237] Initial OAuth2.0/PKCE Provider support --- docs/feature_matrix.rst | 9 +- docs/oauth2/server.rst | 11 ++ oauthlib/common.py | 3 + oauthlib/oauth2/rfc6749/errors.py | 32 +++++ .../rfc6749/grant_types/authorization_code.py | 110 ++++++++++++++++ oauthlib/oauth2/rfc6749/request_validator.py | 108 +++++++++++++-- .../endpoints/test_client_authentication.py | 2 + .../test_credentials_preservation.py | 1 + .../rfc6749/endpoints/test_error_responses.py | 1 + .../test_resource_owner_association.py | 1 + .../rfc6749/endpoints/test_scope_handling.py | 1 + .../grant_types/test_authorization_code.py | 124 ++++++++++++++++++ tests/oauth2/rfc6749/test_server.py | 3 + .../core/endpoints/test_claims_handling.py | 1 + .../grant_types/test_authorization_code.py | 1 + .../connect/core/grant_types/test_implicit.py | 2 + tests/openid/connect/core/test_server.py | 2 + 17 files changed, 397 insertions(+), 15 deletions(-) diff --git a/docs/feature_matrix.rst b/docs/feature_matrix.rst index 45010d15..df8cb0e9 100644 --- a/docs/feature_matrix.rst +++ b/docs/feature_matrix.rst @@ -18,14 +18,16 @@ OAuth 2.0 client and provider support for: - `RFC7009`_: Token Revocation - `RFC Draft MAC tokens`_ - OAuth2.0 Provider: `OpenID Connect Core`_ +- OAuth2.0 Provider: `RFC7636`_: Proof Key for Code Exchange by OAuth Public Clients (PKCE) - OAuth2.0 Provider: `RFC7662`_: Token Introspection - OAuth2.0 Provider: `RFC8414`_: Authorization Server Metadata Features to be implemented (any help/PR are welcomed): -- OAuth2.0 Client: `OpenID Connect Core`_ -- OAuth2.0 Client: `RFC7662`_: Token Introspection -- OAuth2.0 Client: `RFC8414`_: Authorization Server Metadata +- OAuth2.0 **Client**: `OpenID Connect Core`_ +- OAuth2.0 **Client**: `RFC7636`_: Proof Key for Code Exchange by OAuth Public Clients (PKCE) +- OAuth2.0 **Client**: `RFC7662`_: Token Introspection +- OAuth2.0 **Client**: `RFC8414`_: Authorization Server Metadata - SAML2 - Bearer JWT as Client Authentication - Dynamic client registration @@ -51,5 +53,6 @@ RSA you are limited to the platforms supported by `cryptography`_. .. _`RFC Draft MAC tokens`: https://tools.ietf.org/id/draft-ietf-oauth-v2-http-mac-02.html .. _`RFC7009`: https://tools.ietf.org/html/rfc7009 .. _`RFC7662`: https://tools.ietf.org/html/rfc7662 +.. _`RFC7636`: https://tools.ietf.org/html/rfc7636 .. _`OpenID Connect Core`: https://openid.net/specs/openid-connect-core-1_0.html .. _`RFC8414`: https://tools.ietf.org/html/rfc8414 diff --git a/docs/oauth2/server.rst b/docs/oauth2/server.rst index 35a58aaf..eca363b7 100644 --- a/docs/oauth2/server.rst +++ b/docs/oauth2/server.rst @@ -246,6 +246,17 @@ the token. expires_at = django.db.models.DateTimeField() +**PKCE Challenge (optional)** + + If you want to support PKCE, you have to associate a `code_challenge` + and a `code_challenge_method` to the actual Authorization Code. + + .. code-block:: python + + challenge = django.db.models.CharField(max_length=100) + challenge_method = django.db.models.CharField(max_length=6) + + 2. Implement a validator ------------------------ diff --git a/oauthlib/common.py b/oauthlib/common.py index bd6ec56f..970d7a5b 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -397,6 +397,9 @@ def __init__(self, uri, http_method='GET', body=None, headers=None, "client_id": None, "client_secret": None, "code": None, + "code_challenge": None, + "code_challenge_method": None, + "code_verifier": None, "extra_credentials": None, "grant_type": None, "redirect_uri": None, diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 7ead3d4c..f7fac5cc 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -180,6 +180,26 @@ class MissingResponseTypeError(InvalidRequestError): description = 'Missing response_type parameter.' +class MissingCodeChallengeError(InvalidRequestError): + """ + If the server requires Proof Key for Code Exchange (PKCE) by OAuth + public clients and the client does not send the "code_challenge" in + the request, the authorization endpoint MUST return the authorization + error response with the "error" value set to "invalid_request". The + "error_description" or the response of "error_uri" SHOULD explain the + nature of error, e.g., code challenge required. + """ + description = 'Code challenge required.' + + +class MissingCodeVerifierError(InvalidRequestError): + """ + The request to the token endpoint, when PKCE is enabled, has + the parameter `code_verifier` REQUIRED. + """ + description = 'Code verifier required.' + + class AccessDeniedError(OAuth2Error): """ The resource owner or authorization server denied the request. @@ -196,6 +216,18 @@ class UnsupportedResponseTypeError(OAuth2Error): error = 'unsupported_response_type' +class UnsupportedCodeChallengeMethodError(InvalidRequestError): + """ + If the server supporting PKCE does not support the requested + transformation, the authorization endpoint MUST return the + authorization error response with "error" value set to + "invalid_request". The "error_description" or the response of + "error_uri" SHOULD explain the nature of error, e.g., transform + algorithm not supported. + """ + description = 'Transform algorithm not supported.' + + class InvalidScopeError(OAuth2Error): """ The requested scope is invalid, unknown, or malformed. diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 8ebae498..0df7c6c2 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -5,6 +5,8 @@ """ from __future__ import absolute_import, unicode_literals +import base64 +import hashlib import json import logging @@ -17,6 +19,52 @@ log = logging.getLogger(__name__) +def code_challenge_method_s256(verifier, challenge): + """ + If the "code_challenge_method" from `Section 4.3`_ was "S256", the + received "code_verifier" is hashed by SHA-256, base64url-encoded, and + then compared to the "code_challenge", i.e.: + + BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge + + How to implement a base64url-encoding + function without padding, based upon the standard base64-encoding + function that uses padding. + + To be concrete, example C# code implementing these functions is shown + below. Similar code could be used in other languages. + + static string base64urlencode(byte [] arg) + { + string s = Convert.ToBase64String(arg); // Regular base64 encoder + s = s.Split('=')[0]; // Remove any trailing '='s + s = s.Replace('+', '-'); // 62nd char of encoding + s = s.Replace('/', '_'); // 63rd char of encoding + return s; + } + + In python urlsafe_b64encode is already replacing '+' and '/', but preserve + the trailing '='. So we have to remove it. + + .. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3 + """ + return base64.urlsafe_b64encode( + hashlib.sha256(verifier.encode()).digest() + ).decode().rstrip('=') == challenge + + +def code_challenge_method_plain(verifier, challenge): + """ + If the "code_challenge_method" from `Section 4.3`_ was "plain", they are + compared directly, i.e.: + + code_verifier == code_challenge. + + .. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3 + """ + return verifier == challenge + + class AuthorizationCodeGrant(GrantTypeBase): """`Authorization Code Grant`_ @@ -91,12 +139,28 @@ class AuthorizationCodeGrant(GrantTypeBase): step (C). If valid, the authorization server responds back with an access token and, optionally, a refresh token. + OAuth 2.0 public clients utilizing the Authorization Code Grant are + susceptible to the authorization code interception attack. + + A technique to mitigate against the threat through the use of Proof Key for Code + Exchange (PKCE, pronounced "pixy") is implemented in the current oauthlib + implementation. + .. _`Authorization Code Grant`: https://tools.ietf.org/html/rfc6749#section-4.1 + .. _`PKCE`: https://tools.ietf.org/html/rfc7636 """ default_response_mode = 'query' response_types = ['code'] + # This dict below is private because as RFC mention it: + # "S256" is Mandatory To Implement (MTI) on the server. + # + _code_challenge_methods = { + 'plain': code_challenge_method_plain, + 'S256': code_challenge_method_s256 + } + def create_authorization_code(self, request): """ Generates an authorization grant represented as a dictionary. @@ -350,6 +414,20 @@ def validate_authorization_request(self, request): request.client_id, request.response_type) raise errors.UnauthorizedClientError(request=request) + # OPTIONAL. Validate PKCE request or reply with "error"/"invalid_request" + # https://tools.ietf.org/html/rfc6749#section-4.4.1 + if self.request_validator.is_pkce_required(request.client_id, request) is True: + if request.code_challenge is None: + raise errors.MissingCodeChallengeError(request=request) + + if request.code_challenge is not None: + # OPTIONAL, defaults to "plain" if not present in the request. + if request.code_challenge_method is None: + request.code_challenge_method = "plain" + + if request.code_challenge_method not in self._code_challenge_methods: + raise errors.UnsupportedCodeChallengeMethodError(request=request) + # OPTIONAL. The scope of the access request as described by Section 3.3 # https://tools.ietf.org/html/rfc6749#section-3.3 self.validate_scopes(request) @@ -422,6 +500,33 @@ def validate_token_request(self, request): request.client_id, request.client, request.scopes) raise errors.InvalidGrantError(request=request) + # OPTIONAL. Validate PKCE code_verifier + challenge = self.request_validator.get_code_challenge(request.code, request) + + if challenge is not None: + if request.code_verifier is None: + raise errors.MissingCodeVerifierError(request=request) + + challenge_method = self.request_validator.get_code_challenge_method(request.code, request) + if challenge_method is None: + raise errors.InvalidGrantError(request=request, description="Challenge method not found") + + if challenge_method not in self._code_challenge_methods: + raise errors.ServerError( + description="code_challenge_method {} is not supported.".format(challenge_method), + request=request + ) + + if not self.validate_code_challenge(challenge, + challenge_method, + request.code_verifier): + log.debug('request provided a invalid code_verifier.') + raise errors.InvalidGrantError(request=request) + elif self.request_validator.is_pkce_required(request.client_id, request) is True: + if request.code_verifier is None: + raise errors.MissingCodeVerifierError(request=request) + raise errors.InvalidGrantError(request=request, description="Challenge not found") + for attr in ('user', 'scopes'): if getattr(request, attr, None) is None: log.debug('request.%s was not set on code validation.', attr) @@ -449,3 +554,8 @@ def validate_token_request(self, request): for validator in self.custom_validators.post_token: validator(request) + + def validate_code_challenge(self, challenge, challenge_method, verifier): + if challenge_method in self._code_challenge_methods: + return self._code_challenge_methods[challenge_method](verifier, challenge) + raise NotImplementedError('Unknown challenge_method %s' % challenge_method) diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 2cf1b82c..193a9e1f 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -262,25 +262,29 @@ def save_authorization_code(self, client_id, code, request, *args, **kwargs): """Persist the authorization_code. The code should at minimum be stored with: - - the client_id (client_id) - - the redirect URI used (request.redirect_uri) - - a resource owner / user (request.user) - - the authorized scopes (request.scopes) - - the client state, if given (code.get('state')) + - the client_id (``client_id``) + - the redirect URI used (``request.redirect_uri``) + - a resource owner / user (``request.user``) + - the authorized scopes (``request.scopes``) + - the client state, if given (``code.get('state')``) + + To support PKCE, you MUST associate the code with: + - Code Challenge (``request.code_challenge``) and + - Code Challenge Method (``request.code_challenge_method``) - The 'code' argument is actually a dictionary, containing at least a - 'code' key with the actual authorization code: + The ``code`` argument is actually a dictionary, containing at least a + ``code`` key with the actual authorization code: - {'code': 'sdf345jsdf0934f'} + ``{'code': 'sdf345jsdf0934f'}`` - It may also have a 'state' key containing a nonce for the client, if it + It may also have a ``state`` key containing a nonce for the client, if it chose to send one. That value should be saved and used in - 'validate_code'. + ``.validate_code``. - It may also have a 'claims' parameter which, when present, will be a dict + It may also have a ``claims`` parameter which, when present, will be a dict deserialized from JSON as described at http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter - This value should be saved in this method and used again in 'validate_code'. + This value should be saved in this method and used again in ``.validate_code``. :param client_id: Unicode client identifier. :param code: A dict of the authorization code grant and, optionally, state. @@ -564,6 +568,11 @@ def validate_code(self, client_id, code, client, request, *args, **kwargs): The request.claims property, if it was given, should assigned a dict. + If PKCE is enabled (see 'is_pkce_required' and 'save_authorization_code') + you MUST set the following based on the information stored: + - request.code_challenge + - request.code_challenge_method + :param client_id: Unicode client identifier. :param code: Unicode authorization code. :param client: Client object set by you, see ``.authenticate_client``. @@ -742,3 +751,78 @@ def validate_user_match(self, id_token_hint, scopes, claims, request): - OpenIDConnectHybrid """ raise NotImplementedError('Subclasses must implement this method.') + + def is_pkce_required(self, client_id, request): + """Determine if current request requires PKCE. Default, False. + This is called for both "authorization" and "token" requests. + + Override this method by ``return True`` to enable PKCE for everyone. + You might want to enable it only for public clients. + Note that PKCE can also be used in addition of a client authentication. + + OAuth 2.0 public clients utilizing the Authorization Code Grant are + susceptible to the authorization code interception attack. This + specification describes the attack as well as a technique to mitigate + against the threat through the use of Proof Key for Code Exchange + (PKCE, pronounced "pixy"). See `RFC7636`_. + + :param client_id: Client identifier. + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :rtype: True or False + + Method is used by: + - Authorization Code Grant + + .. _`RFC7636`: https://tools.ietf.org/html/rfc7636 + """ + return False + + def get_code_challenge(self, code, request): + """Is called for every "token" requests. + + When the server issues the authorization code in the authorization + response, it MUST associate the ``code_challenge`` and + ``code_challenge_method`` values with the authorization code so it can + be verified later. + + Typically, the ``code_challenge`` and ``code_challenge_method`` values + are stored in encrypted form in the ``code`` itself but could + alternatively be stored on the server associated with the code. The + server MUST NOT include the ``code_challenge`` value in client requests + in a form that other entities can extract. + + Return the ``code_challenge`` associated to the code. + If ``None`` is returned, code is considered to not be associated to any + challenges. + + :param code: Authorization code. + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :rtype: code_challenge string + + Method is used by: + - Authorization Code Grant - when PKCE is active + + """ + return None + + def get_code_challenge_method(self, code, request): + """Is called during the "token" request processing, when a + ``code_verifier`` and a ``code_challenge`` has been provided. + + See ``.get_code_challenge``. + + Must return ``plain`` or ``S256``. You can return a custom value if you have + implemented your own ``AuthorizationCodeGrant`` class. + + :param code: Authorization code. + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :rtype: code_challenge_method string + + Method is used by: + - Authorization Code Grant - when PKCE is active + + """ + raise NotImplementedError('Subclasses must implement this method.') diff --git a/tests/oauth2/rfc6749/endpoints/test_client_authentication.py b/tests/oauth2/rfc6749/endpoints/test_client_authentication.py index e9a06738..48c5f5ae 100644 --- a/tests/oauth2/rfc6749/endpoints/test_client_authentication.py +++ b/tests/oauth2/rfc6749/endpoints/test_client_authentication.py @@ -32,6 +32,8 @@ def inspect_client(self, request, refresh_token=False): def setUp(self): self.validator = mock.MagicMock(spec=RequestValidator) + self.validator.is_pkce_required.return_value = False + self.validator.get_code_challenge.return_value = None self.validator.get_default_redirect_uri.return_value = 'http://i.b./path' self.web = WebApplicationServer(self.validator, token_generator=self.inspect_client) diff --git a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py index 50c2956d..1a2f66bc 100644 --- a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py +++ b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py @@ -24,6 +24,7 @@ class PreservationTest(TestCase): def setUp(self): self.validator = mock.MagicMock(spec=RequestValidator) self.validator.get_default_redirect_uri.return_value = self.DEFAULT_REDIRECT_URI + self.validator.get_code_challenge.return_value = None self.validator.authenticate_client.side_effect = self.set_client self.web = WebApplicationServer(self.validator) self.mobile = MobileApplicationServer(self.validator) diff --git a/tests/oauth2/rfc6749/endpoints/test_error_responses.py b/tests/oauth2/rfc6749/endpoints/test_error_responses.py index ef05c4d4..a249cb1c 100644 --- a/tests/oauth2/rfc6749/endpoints/test_error_responses.py +++ b/tests/oauth2/rfc6749/endpoints/test_error_responses.py @@ -24,6 +24,7 @@ def set_client(self, request): def setUp(self): self.validator = mock.MagicMock(spec=RequestValidator) self.validator.get_default_redirect_uri.return_value = None + self.validator.get_code_challenge.return_value = None self.web = WebApplicationServer(self.validator) self.mobile = MobileApplicationServer(self.validator) self.legacy = LegacyApplicationServer(self.validator) diff --git a/tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py b/tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py index d30ec9d7..e8232865 100644 --- a/tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py +++ b/tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py @@ -46,6 +46,7 @@ def inspect_client(self, request, refresh_token=False): def setUp(self): self.validator = mock.MagicMock(spec=RequestValidator) self.validator.get_default_redirect_uri.return_value = 'http://i.b./path' + self.validator.get_code_challenge.return_value = None self.validator.authenticate_client.side_effect = self.set_client self.web = WebApplicationServer(self.validator, token_generator=self.inspect_client) diff --git a/tests/oauth2/rfc6749/endpoints/test_scope_handling.py b/tests/oauth2/rfc6749/endpoints/test_scope_handling.py index 8490c03f..4f279637 100644 --- a/tests/oauth2/rfc6749/endpoints/test_scope_handling.py +++ b/tests/oauth2/rfc6749/endpoints/test_scope_handling.py @@ -42,6 +42,7 @@ def set_client(self, request): def setUp(self): self.validator = mock.MagicMock(spec=RequestValidator) self.validator.get_default_redirect_uri.return_value = TestScopeHandling.DEFAULT_REDIRECT_URI + self.validator.get_code_challenge.return_value = None self.validator.authenticate_client.side_effect = self.set_client self.server = Server(self.validator) self.web = WebApplicationServer(self.validator) diff --git a/tests/oauth2/rfc6749/grant_types/test_authorization_code.py b/tests/oauth2/rfc6749/grant_types/test_authorization_code.py index acb23acd..00e2b6d9 100644 --- a/tests/oauth2/rfc6749/grant_types/test_authorization_code.py +++ b/tests/oauth2/rfc6749/grant_types/test_authorization_code.py @@ -8,6 +8,7 @@ from oauthlib.common import Request from oauthlib.oauth2.rfc6749 import errors from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant +from oauthlib.oauth2.rfc6749.grant_types import authorization_code from oauthlib.oauth2.rfc6749.tokens import BearerToken from ....unittest import TestCase @@ -27,6 +28,8 @@ def setUp(self): self.request.redirect_uri = 'https://a.b/cb' self.mock_validator = mock.MagicMock() + self.mock_validator.is_pkce_required.return_value = False + self.mock_validator.get_code_challenge.return_value = None self.mock_validator.authenticate_client.side_effect = self.set_client self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator) @@ -200,3 +203,124 @@ def test_invalid_redirect_uri(self): self.mock_validator.confirm_redirect_uri.return_value = False self.assertRaises(errors.MismatchingRedirectURIError, self.auth.validate_token_request, self.request) + + # PKCE validate_authorization_request + def test_pkce_challenge_missing(self): + self.mock_validator.is_pkce_required.return_value = True + self.assertRaises(errors.MissingCodeChallengeError, + self.auth.validate_authorization_request, self.request) + + def test_pkce_default_method(self): + for required in [True, False]: + self.mock_validator.is_pkce_required.return_value = required + self.request.code_challenge = "present" + _, ri = self.auth.validate_authorization_request(self.request) + self.assertIsNotNone(ri["request"].code_challenge_method) + self.assertEqual(ri["request"].code_challenge_method, "plain") + + def test_pkce_wrong_method(self): + for required in [True, False]: + self.mock_validator.is_pkce_required.return_value = required + self.request.code_challenge = "present" + self.request.code_challenge_method = "foobar" + self.assertRaises(errors.UnsupportedCodeChallengeMethodError, + self.auth.validate_authorization_request, self.request) + + # PKCE validate_token_request + def test_pkce_verifier_missing(self): + self.mock_validator.is_pkce_required.return_value = True + self.assertRaises(errors.MissingCodeVerifierError, + self.auth.validate_token_request, self.request) + + # PKCE validate_token_request + def test_pkce_required_verifier_missing_challenge_missing(self): + self.mock_validator.is_pkce_required.return_value = True + self.request.code_verifier = None + self.mock_validator.get_code_challenge.return_value = None + self.assertRaises(errors.MissingCodeVerifierError, + self.auth.validate_token_request, self.request) + + def test_pkce_required_verifier_missing_challenge_valid(self): + self.mock_validator.is_pkce_required.return_value = True + self.request.code_verifier = None + self.mock_validator.get_code_challenge.return_value = "foo" + self.assertRaises(errors.MissingCodeVerifierError, + self.auth.validate_token_request, self.request) + + def test_pkce_required_verifier_valid_challenge_missing(self): + self.mock_validator.is_pkce_required.return_value = True + self.request.code_verifier = "foobar" + self.mock_validator.get_code_challenge.return_value = None + self.assertRaises(errors.InvalidGrantError, + self.auth.validate_token_request, self.request) + + def test_pkce_required_verifier_valid_challenge_valid_method_valid(self): + self.mock_validator.is_pkce_required.return_value = True + self.request.code_verifier = "foobar" + self.mock_validator.get_code_challenge.return_value = "foobar" + self.mock_validator.get_code_challenge_method.return_value = "plain" + self.auth.validate_token_request(self.request) + + def test_pkce_required_verifier_invalid_challenge_valid_method_valid(self): + self.mock_validator.is_pkce_required.return_value = True + self.request.code_verifier = "foobar" + self.mock_validator.get_code_challenge.return_value = "raboof" + self.mock_validator.get_code_challenge_method.return_value = "plain" + self.assertRaises(errors.InvalidGrantError, + self.auth.validate_token_request, self.request) + + def test_pkce_required_verifier_valid_challenge_valid_method_wrong(self): + self.mock_validator.is_pkce_required.return_value = True + self.request.code_verifier = "present" + self.mock_validator.get_code_challenge.return_value = "foobar" + self.mock_validator.get_code_challenge_method.return_value = "cryptic_method" + self.assertRaises(errors.ServerError, + self.auth.validate_token_request, self.request) + + def test_pkce_verifier_valid_challenge_valid_method_missing(self): + self.mock_validator.is_pkce_required.return_value = True + self.request.code_verifier = "present" + self.mock_validator.get_code_challenge.return_value = "foobar" + self.mock_validator.get_code_challenge_method.return_value = None + self.assertRaises(errors.InvalidGrantError, + self.auth.validate_token_request, self.request) + + def test_pkce_optional_verifier_valid_challenge_missing(self): + self.mock_validator.is_pkce_required.return_value = False + self.request.code_verifier = "present" + self.mock_validator.get_code_challenge.return_value = None + self.auth.validate_token_request(self.request) + + def test_pkce_optional_verifier_missing_challenge_valid(self): + self.mock_validator.is_pkce_required.return_value = False + self.request.code_verifier = None + self.mock_validator.get_code_challenge.return_value = "foobar" + self.assertRaises(errors.MissingCodeVerifierError, + self.auth.validate_token_request, self.request) + + # PKCE functions + def test_wrong_code_challenge_method_plain(self): + self.assertFalse(authorization_code.code_challenge_method_plain("foo", "bar")) + + def test_correct_code_challenge_method_plain(self): + self.assertTrue(authorization_code.code_challenge_method_plain("foo", "foo")) + + def test_wrong_code_challenge_method_s256(self): + self.assertFalse(authorization_code.code_challenge_method_s256("foo", "bar")) + + def test_correct_code_challenge_method_s256(self): + # "abcd" as verifier gives a '+' to base64 + self.assertTrue( + authorization_code.code_challenge_method_s256("abcd", + "iNQmb9TmM40TuEX88olXnSCciXgjuSF9o-Fhk28DFYk") + ) + # "/" as verifier gives a '/' and '+' to base64 + self.assertTrue( + authorization_code.code_challenge_method_s256("/", + "il7asoJjJEMhngUeSt4tHVu8Zxx4EFG_FDeJfL3-oPE") + ) + # Example from PKCE RFCE + self.assertTrue( + authorization_code.code_challenge_method_s256("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", + "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM") + ) diff --git a/tests/oauth2/rfc6749/test_server.py b/tests/oauth2/rfc6749/test_server.py index bc7a2b7c..b623a9ba 100644 --- a/tests/oauth2/rfc6749/test_server.py +++ b/tests/oauth2/rfc6749/test_server.py @@ -23,6 +23,7 @@ class AuthorizationEndpointTest(TestCase): def setUp(self): self.mock_validator = mock.MagicMock() + self.mock_validator.get_code_challenge.return_value = None self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) auth_code = AuthorizationCodeGrant( request_validator=self.mock_validator) @@ -117,6 +118,7 @@ def set_user(request): self.mock_validator = mock.MagicMock() self.mock_validator.authenticate_client.side_effect = set_user + self.mock_validator.get_code_challenge.return_value = None self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) auth_code = AuthorizationCodeGrant( request_validator=self.mock_validator) @@ -218,6 +220,7 @@ def set_user(request): return True self.mock_validator = mock.MagicMock() + self.mock_validator.get_code_challenge.return_value = None self.mock_validator.authenticate_client.side_effect = set_user self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) diff --git a/tests/openid/connect/core/endpoints/test_claims_handling.py b/tests/openid/connect/core/endpoints/test_claims_handling.py index d5908a88..270ef69c 100644 --- a/tests/openid/connect/core/endpoints/test_claims_handling.py +++ b/tests/openid/connect/core/endpoints/test_claims_handling.py @@ -56,6 +56,7 @@ def save_claims_with_bearer_token(self, token, request, *args, **kwargs): def setUp(self): self.validator = mock.MagicMock(spec=RequestValidator) + self.validator.get_code_challenge.return_value = None self.validator.get_default_redirect_uri.return_value = TestClaimsHandling.DEFAULT_REDIRECT_URI self.validator.authenticate_client.side_effect = self.set_client diff --git a/tests/openid/connect/core/grant_types/test_authorization_code.py b/tests/openid/connect/core/grant_types/test_authorization_code.py index 9bbe7fb8..c3c78242 100644 --- a/tests/openid/connect/core/grant_types/test_authorization_code.py +++ b/tests/openid/connect/core/grant_types/test_authorization_code.py @@ -43,6 +43,7 @@ def setUp(self): self.mock_validator = mock.MagicMock() self.mock_validator.authenticate_client.side_effect = self.set_client + self.mock_validator.get_code_challenge.return_value = None self.mock_validator.get_id_token.side_effect = get_id_token_mock self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator) diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py index c369bb60..7ab198ac 100644 --- a/tests/openid/connect/core/grant_types/test_implicit.py +++ b/tests/openid/connect/core/grant_types/test_implicit.py @@ -120,6 +120,7 @@ class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): def setUp(self): super(OpenIDHybridCodeIdTokenTest, self).setUp() + self.mock_validator.get_code_challenge.return_value = None self.request.response_type = 'code id_token' self.auth = HybridGrant(request_validator=self.mock_validator) token = 'MOCKED_TOKEN' @@ -131,6 +132,7 @@ class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): def setUp(self): super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() + self.mock_validator.get_code_challenge.return_value = None self.request.response_type = 'code id_token token' self.auth = HybridGrant(request_validator=self.mock_validator) token = 'MOCKED_TOKEN' diff --git a/tests/openid/connect/core/test_server.py b/tests/openid/connect/core/test_server.py index a83f22df..ffab7b0b 100644 --- a/tests/openid/connect/core/test_server.py +++ b/tests/openid/connect/core/test_server.py @@ -21,6 +21,7 @@ class AuthorizationEndpointTest(TestCase): def setUp(self): self.mock_validator = mock.MagicMock() + self.mock_validator.get_code_challenge.return_value = None self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) auth_code = AuthorizationCodeGrant(request_validator=self.mock_validator) auth_code.save_authorization_code = mock.MagicMock() @@ -122,6 +123,7 @@ def set_user(request): self.mock_validator = mock.MagicMock() self.mock_validator.authenticate_client.side_effect = set_user + self.mock_validator.get_code_challenge.return_value = None self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock()) auth_code = AuthorizationCodeGrant( request_validator=self.mock_validator) From cf3cf407be774405f66188219eb1653c723e294b Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 30 Nov 2018 15:12:04 +0100 Subject: [PATCH 116/237] Add OAuth2 Provider Server Metadata for PKCE. --- oauthlib/oauth2/rfc6749/endpoints/metadata.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py index 6d77b9f5..68733345 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/metadata.py +++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py @@ -104,6 +104,8 @@ def validate_metadata_authorization(self, claims, endpoint): self.validate_metadata(claims, "response_types_supported", is_required=True, is_list=True) self.validate_metadata(claims, "response_modes_supported", is_list=True) if "code" in claims["response_types_supported"]: + claims.setdefault("code_challenge_methods_supported", + list(endpoint._response_types["code"]._code_challenge_methods.keys())) self.validate_metadata(claims, "code_challenge_methods_supported", is_list=True) self.validate_metadata(claims, "authorization_endpoint", is_required=True, is_url=True) From 6bd865b36dc64caaa8aab9788742c9d54ce81c4d Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 30 Nov 2018 15:15:50 +0100 Subject: [PATCH 117/237] Add Server metadata test and fix metadata. Fix grant_types_supported which must include "implicit" even if it is not a grant_type in oauthlib sense. Removed internal "none" field value from the list of response_types. --- oauthlib/oauth2/rfc6749/endpoints/metadata.py | 12 +++-- .../oauth2/rfc6749/endpoints/test_metadata.py | 53 +++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py index 68733345..84ddf8f8 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/metadata.py +++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py @@ -89,17 +89,19 @@ def validate_metadata(self, array, key, is_required=False, is_list=False, is_url raise ValueError("array {}: {} must contains only string (not {})".format(key, array[key], elem)) def validate_metadata_token(self, claims, endpoint): - claims.setdefault("grant_types_supported", list(endpoint._grant_types.keys())) + self._grant_types += list(endpoint._grant_types.keys()) claims.setdefault("token_endpoint_auth_methods_supported", ["client_secret_post", "client_secret_basic"]) - self.validate_metadata(claims, "grant_types_supported", is_list=True) self.validate_metadata(claims, "token_endpoint_auth_methods_supported", is_list=True) self.validate_metadata(claims, "token_endpoint_auth_signing_alg_values_supported", is_list=True) self.validate_metadata(claims, "token_endpoint", is_required=True, is_url=True) def validate_metadata_authorization(self, claims, endpoint): - claims.setdefault("response_types_supported", list(self._response_types.keys())) + claims.setdefault("response_types_supported", + list(filter(lambda x: x != "none", endpoint._response_types.keys()))) claims.setdefault("response_modes_supported", ["query", "fragment"]) + if "token" in claims["response_types_supported"]: + self._grant_types.append("implicit") self.validate_metadata(claims, "response_types_supported", is_required=True, is_list=True) self.validate_metadata(claims, "response_modes_supported", is_list=True) @@ -183,6 +185,7 @@ def validate_metadata_server(self): self.validate_metadata(claims, "op_policy_uri", is_url=True) self.validate_metadata(claims, "op_tos_uri", is_url=True) + self._grant_types = [] for endpoint in self.endpoints: if isinstance(endpoint, TokenEndpoint): self.validate_metadata_token(claims, endpoint) @@ -192,4 +195,7 @@ def validate_metadata_server(self): self.validate_metadata_revocation(claims, endpoint) if isinstance(endpoint, IntrospectEndpoint): self.validate_metadata_introspection(claims, endpoint) + + claims.setdefault("grant_types_supported", self._grant_types) + self.validate_metadata(claims, "grant_types_supported", is_list=True) return claims diff --git a/tests/oauth2/rfc6749/endpoints/test_metadata.py b/tests/oauth2/rfc6749/endpoints/test_metadata.py index 301e8469..5174b2dc 100644 --- a/tests/oauth2/rfc6749/endpoints/test_metadata.py +++ b/tests/oauth2/rfc6749/endpoints/test_metadata.py @@ -3,6 +3,7 @@ from oauthlib.oauth2 import MetadataEndpoint from oauthlib.oauth2 import TokenEndpoint +from oauthlib.oauth2 import Server from ....unittest import TestCase @@ -36,3 +37,55 @@ def test_mandatory_fields(self): metadata = MetadataEndpoint([], self.metadata) self.assertIn("issuer", metadata.claims) self.assertEqual(metadata.claims["issuer"], 'https://foo.bar') + + def test_server_metadata(self): + endpoint = Server(None) + metadata = MetadataEndpoint([endpoint], { + "issuer": 'https://foo.bar', + "authorization_endpoint": "https://foo.bar/authorize", + "introspection_endpoint": "https://foo.bar/introspect", + "revocation_endpoint": "https://foo.bar/revoke", + "token_endpoint": "https://foo.bar/token", + "jwks_uri": "https://foo.bar/certs", + "scopes_supported": ["email", "profile"] + }) + self.assertEqual(metadata.claims, { + "issuer": "https://foo.bar", + "authorization_endpoint": "https://foo.bar/authorize", + "introspection_endpoint": "https://foo.bar/introspect", + "revocation_endpoint": "https://foo.bar/revoke", + "token_endpoint": "https://foo.bar/token", + "jwks_uri": "https://foo.bar/certs", + "scopes_supported": ["email", "profile"], + "grant_types_supported": [ + "authorization_code", + "password", + "client_credentials", + "refresh_token", + "implicit" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_post", + "client_secret_basic" + ], + "response_types_supported": [ + "code", + "token" + ], + "response_modes_supported": [ + "query", + "fragment" + ], + "code_challenge_methods_supported": [ + "plain", + "S256" + ], + "revocation_endpoint_auth_methods_supported": [ + "client_secret_post", + "client_secret_basic" + ], + "introspection_endpoint_auth_methods_supported": [ + "client_secret_post", + "client_secret_basic" + ] + }) From d7891e70a7593bc428510f66d8c1e60ff3731c30 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 30 Nov 2018 15:36:57 +0100 Subject: [PATCH 118/237] Sort dict and list in dict values for py27/36 compat --- tests/oauth2/rfc6749/endpoints/test_metadata.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/oauth2/rfc6749/endpoints/test_metadata.py b/tests/oauth2/rfc6749/endpoints/test_metadata.py index 5174b2dc..875316ae 100644 --- a/tests/oauth2/rfc6749/endpoints/test_metadata.py +++ b/tests/oauth2/rfc6749/endpoints/test_metadata.py @@ -49,7 +49,7 @@ def test_server_metadata(self): "jwks_uri": "https://foo.bar/certs", "scopes_supported": ["email", "profile"] }) - self.assertEqual(metadata.claims, { + expected_claims = { "issuer": "https://foo.bar", "authorization_endpoint": "https://foo.bar/authorize", "introspection_endpoint": "https://foo.bar/introspect", @@ -88,4 +88,12 @@ def test_server_metadata(self): "client_secret_post", "client_secret_basic" ] - }) + } + + def sort_list(claims): + for k in claims.keys(): + claims[k] = sorted(claims[k]) + + sort_list(metadata.claims) + sort_list(expected_claims) + self.assertEqual(sorted(metadata.claims.items()), sorted(expected_claims.items())) From ea849f66a92f6ce78cae6574e8d20a19a267ac96 Mon Sep 17 00:00:00 2001 From: Benjamin Pereto Date: Mon, 3 Dec 2018 17:59:25 +0100 Subject: [PATCH 119/237] `invalid_scope` status code should be 400 --- oauthlib/oauth2/rfc6749/errors.py | 4 +++- tests/oauth2/rfc6749/grant_types/test_refresh_token.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 7ead3d4c..678fcff7 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -199,9 +199,11 @@ class UnsupportedResponseTypeError(OAuth2Error): class InvalidScopeError(OAuth2Error): """ The requested scope is invalid, unknown, or malformed. + + https://tools.ietf.org/html/rfc6749#section-5.2 """ error = 'invalid_scope' - status_code = 401 + status_code = 400 class ServerError(OAuth2Error): diff --git a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py index f055c7d7..32a0977c 100644 --- a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py +++ b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py @@ -99,7 +99,7 @@ def test_invalid_scope(self): token = json.loads(body) self.assertEqual(self.mock_validator.save_token.call_count, 0) self.assertEqual(token['error'], 'invalid_scope') - self.assertEqual(status_code, 401) + self.assertEqual(status_code, 400) def test_invalid_token(self): self.mock_validator.validate_refresh_token.return_value = False From 6ed5980962723092d6fb2cbbfdb63a935787e9c1 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Mon, 3 Dec 2018 17:33:31 +0000 Subject: [PATCH 120/237] Support for Python 3.7. --- .travis.yml | 4 ++++ setup.py | 1 + tox.ini | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f46bf439..f4b8bfbd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ language: python +python: 3.7 +dist: xenial sudo: false cache: pip matrix: @@ -11,6 +13,8 @@ matrix: env: TOXENV=py35 - python: 3.6 env: TOXENV=py36 + - python: 3.7 + env: TOXENV=py37 - python: pypy-5.3 env: TOXENV=pypy install: diff --git a/setup.py b/setup.py index 640bbe1c..d2a27a0c 100755 --- a/setup.py +++ b/setup.py @@ -64,6 +64,7 @@ def fread(fn): 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', diff --git a/tox.ini b/tox.ini index c45f6574..35f0983d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36,pypy,docs,readme +envlist = py27,py34,py35,py36,py37,pypy,docs,readme [testenv] deps= From 399ea07ef9674e43aeb0b0e29df59472c86d3de2 Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Mon, 3 Dec 2018 20:49:37 +0000 Subject: [PATCH 121/237] Add PyCharm ignore. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 6f246494..a3f56149 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.pyc +.idea *.sublime-project *.sublime-workspace *.swp @@ -22,6 +23,7 @@ develop-eggs pip-log.txt # Unit test / coverage reports +.cache .coverage .tox coverage From ada6ef467b11e007e61575a2ecaf41547d39d64f Mon Sep 17 00:00:00 2001 From: Pieter Ennes Date: Mon, 3 Dec 2018 20:57:40 +0000 Subject: [PATCH 122/237] Fix PyPy build in Xenial. --- .travis.yml | 4 ++-- tox.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index f4b8bfbd..e304ce62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,8 +15,8 @@ matrix: env: TOXENV=py36 - python: 3.7 env: TOXENV=py37 - - python: pypy-5.3 - env: TOXENV=pypy + - python: pypy3.5 + env: TOXENV=pypy3 install: - pip install -U setuptools - pip install tox coveralls diff --git a/tox.ini b/tox.ini index 35f0983d..47237d8c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36,py37,pypy,docs,readme +envlist = py27,py34,py35,py36,py37,pypy,pypy3,docs,readme [testenv] deps= From ffa87c7ec828e5c0a7c68a2197030f20b15ec621 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 4 Dec 2018 16:08:13 +0100 Subject: [PATCH 123/237] Handle 401 with WWW-Authenticate. Moved wrong 401 into 400. access_denied/unauthorized_client/consent_required/login_required MUST be 400, and not 401. Also, 401 MUST have WWW-Authenticate when set. It could have an impact of processing those in webframeworks. --- oauthlib/oauth2/rfc6749/endpoints/introspect.py | 2 ++ oauthlib/oauth2/rfc6749/endpoints/revocation.py | 2 ++ oauthlib/oauth2/rfc6749/errors.py | 8 ++------ oauthlib/oauth2/rfc6749/grant_types/authorization_code.py | 2 ++ oauthlib/oauth2/rfc6749/grant_types/client_credentials.py | 2 ++ oauthlib/oauth2/rfc6749/grant_types/refresh_token.py | 3 +++ .../grant_types/resource_owner_password_credentials.py | 2 ++ .../oauth2/rfc6749/endpoints/test_introspect_endpoint.py | 4 ++-- .../oauth2/rfc6749/endpoints/test_revocation_endpoint.py | 4 ++-- 9 files changed, 19 insertions(+), 10 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py index 7613acce..ac2e3286 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/introspect.py +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -63,6 +63,8 @@ def create_introspect_response(self, uri, http_method='POST', body=None, log.debug('Token introspect valid for %r.', request) except OAuth2Error as e: log.debug('Client error during validation of %r. %r.', request, e) + if e.status_code == 401: + return {"WWW-Authenticate": "Basic"}, e.json, e.status_code return {}, e.json, e.status_code claims = self.request_validator.introspect_token( diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py index d5b5b782..b13faa38 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py +++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py @@ -69,6 +69,8 @@ def create_revocation_response(self, uri, http_method='POST', body=None, response_body = e.json if self.enable_jsonp and request.callback: response_body = '%s(%s);' % (request.callback, response_body) + if e.status_code == 401: + return {"WWW-Authenticate": "Basic"}, response_body, e.status_code return {}, response_body, e.status_code self.request_validator.revoke_token(request.token, diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index 678fcff7..addcb6d2 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -185,7 +185,6 @@ class AccessDeniedError(OAuth2Error): The resource owner or authorization server denied the request. """ error = 'access_denied' - status_code = 401 class UnsupportedResponseTypeError(OAuth2Error): @@ -198,12 +197,12 @@ class UnsupportedResponseTypeError(OAuth2Error): class InvalidScopeError(OAuth2Error): """ - The requested scope is invalid, unknown, or malformed. + The requested scope is invalid, unknown, or malformed, or + exceeds the scope granted by the resource owner. https://tools.ietf.org/html/rfc6749#section-5.2 """ error = 'invalid_scope' - status_code = 400 class ServerError(OAuth2Error): @@ -261,7 +260,6 @@ class UnauthorizedClientError(OAuth2Error): grant type. """ error = 'unauthorized_client' - status_code = 401 class UnsupportedGrantTypeError(OAuth2Error): @@ -318,7 +316,6 @@ class ConsentRequired(OAuth2Error): completed without displaying a user interface for End-User consent. """ error = 'consent_required' - status_code = 401 class LoginRequired(OAuth2Error): @@ -330,7 +327,6 @@ class LoginRequired(OAuth2Error): completed without displaying a user interface for End-User authentication. """ error = 'login_required' - status_code = 401 class CustomOAuth2Error(OAuth2Error): diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 8ebae498..334ed564 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -243,6 +243,8 @@ def create_token_response(self, request, token_handler): log.debug('Token request validation ok for %r.', request) except errors.OAuth2Error as e: log.debug('Client error during validation of %r. %r.', request, e) + if e.status_code == 401: + headers.update({"WWW-Authenticate": "Basic"}) return headers, e.json, e.status_code token = token_handler.create_token(request, refresh_token=self.refresh_token, save_token=False) diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py index 7d4f74cb..54dbebc5 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py @@ -77,6 +77,8 @@ def create_token_response(self, request, token_handler): self.validate_token_request(request) except errors.OAuth2Error as e: log.debug('Client error in token request. %s.', e) + if e.status_code == 401: + headers.update({"WWW-Authenticate": "Basic"}) return headers, e.json, e.status_code token = token_handler.create_token(request, refresh_token=False, save_token=False) diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py index 5f7382a7..d2b3f6f2 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -63,6 +63,9 @@ def create_token_response(self, request, token_handler): log.debug('Validating refresh token request, %r.', request) self.validate_token_request(request) except errors.OAuth2Error as e: + log.debug('Client error in token request, %s.', e) + if e.status_code == 401: + headers.update({"WWW-Authenticate": "Basic"}) return headers, e.json, e.status_code token = token_handler.create_token(request, diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py index 87e80152..931d76c8 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py @@ -105,6 +105,8 @@ def create_token_response(self, request, token_handler): self.validate_token_request(request) except errors.OAuth2Error as e: log.debug('Client error in token request, %s.', e) + if e.status_code == 401: + headers.update({"WWW-Authenticate": "Basic"}) return headers, e.json, e.status_code token = token_handler.create_token(request, self.refresh_token, save_token=False) diff --git a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py index 7ec81905..f7c80338 100644 --- a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py @@ -86,7 +86,7 @@ def test_introspect_token_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_introspect_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {}) + self.assertEqual(h, {"WWW-Authenticate": "Basic"}) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) @@ -109,7 +109,7 @@ def test_introspect_token_public_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_introspect_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {}) + self.assertEqual(h, {"WWW-Authenticate": "Basic"}) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) diff --git a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py index 77f56629..db562c86 100644 --- a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py @@ -49,7 +49,7 @@ def test_revoke_token_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_revocation_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {}) + self.assertEqual(h, {"WWW-Authenticate": "Basic"}) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) @@ -72,7 +72,7 @@ def test_revoke_token_public_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_revocation_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {}) + self.assertEqual(h, {"WWW-Authenticate": "Basic"}) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) From 14c970245cff3de7d66be1e3711af4abcc6b1f0b Mon Sep 17 00:00:00 2001 From: mlboy Date: Wed, 12 Dec 2018 11:38:17 +0800 Subject: [PATCH 124/237] change: grant_type as attribute --- oauthlib/oauth2/rfc6749/clients/backend_application.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py index cd46f128..a000ecf5 100644 --- a/oauthlib/oauth2/rfc6749/clients/backend_application.py +++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py @@ -29,7 +29,9 @@ class BackendApplicationClient(Client): Since the client authentication is used as the authorization grant, no additional authorization request is needed. """ - + + grant_type = 'client_credentials' + def prepare_request_body(self, body='', scope=None, include_client_id=None, **kwargs): """Add the client credentials to the request body. @@ -69,5 +71,5 @@ def prepare_request_body(self, body='', scope=None, """ kwargs['client_id'] = self.client_id kwargs['include_client_id'] = include_client_id - return prepare_token_request('client_credentials', body=body, + return prepare_token_request(self.grant_type, body=body, scope=scope, **kwargs) From 84389ac54ff195dabe4a0aa834598972b569f4f7 Mon Sep 17 00:00:00 2001 From: mlboy Date: Wed, 12 Dec 2018 11:45:15 +0800 Subject: [PATCH 125/237] change: grant_type as attribute --- oauthlib/oauth2/rfc6749/clients/legacy_application.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/clients/legacy_application.py b/oauthlib/oauth2/rfc6749/clients/legacy_application.py index a13927a2..2449363a 100644 --- a/oauthlib/oauth2/rfc6749/clients/legacy_application.py +++ b/oauthlib/oauth2/rfc6749/clients/legacy_application.py @@ -34,6 +34,8 @@ class LegacyApplicationClient(Client): credentials is beyond the scope of this specification. The client MUST discard the credentials once an access token has been obtained. """ + + grant_type = 'password' def __init__(self, client_id, **kwargs): super(LegacyApplicationClient, self).__init__(client_id, **kwargs) @@ -79,5 +81,5 @@ def prepare_request_body(self, username, password, body='', scope=None, """ kwargs['client_id'] = self.client_id kwargs['include_client_id'] = include_client_id - return prepare_token_request('password', body=body, username=username, + return prepare_token_request(self.grant_type, body=body, username=username, password=password, scope=scope, **kwargs) From 4f2c7adcc0eba360054d27f48728bb47206a96d3 Mon Sep 17 00:00:00 2001 From: mlboy Date: Wed, 12 Dec 2018 11:48:13 +0800 Subject: [PATCH 126/237] change: grant_type as attribute --- oauthlib/oauth2/rfc6749/clients/web_application.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index 487e3a04..0cd39ce5 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -34,6 +34,8 @@ class WebApplicationClient(Client): browser) and capable of receiving incoming requests (via redirection) from the authorization server. """ + + grant_type = 'authorization_code' def __init__(self, client_id, code=None, **kwargs): super(WebApplicationClient, self).__init__(client_id, **kwargs) @@ -151,7 +153,7 @@ def prepare_request_body(self, code=None, redirect_uri=None, body='', kwargs['client_id'] = self.client_id kwargs['include_client_id'] = include_client_id - return prepare_token_request('authorization_code', code=code, body=body, + return prepare_token_request(self.grant_type, code=code, body=body, redirect_uri=redirect_uri, **kwargs) def parse_request_uri_response(self, uri, state=None): From d8d5fe13ce4d29009f79665cb02a932703d9402f Mon Sep 17 00:00:00 2001 From: mlboy Date: Wed, 12 Dec 2018 11:54:19 +0800 Subject: [PATCH 127/237] change: grant_type as attribute use refresh_token_key as attribute --- oauthlib/oauth2/rfc6749/clients/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index d8ded505..1a50644e 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -47,6 +47,7 @@ class Client(object): Python, this is usually :py:class:`oauthlib.oauth2.WebApplicationClient`. """ + refresh_token_key = 'refresh_token' def __init__(self, client_id, default_token_placement=AUTH_HEADER, @@ -435,7 +436,7 @@ def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs resource owner. """ refresh_token = refresh_token or self.refresh_token - return prepare_token_request('refresh_token', body=body, scope=scope, + return prepare_token_request(self.refresh_token_key, body=body, scope=scope, refresh_token=refresh_token, **kwargs) def _add_bearer_token(self, uri, http_method='GET', body=None, From cb9126fa94d2ac5f798a1e1894709bf1f3cd898d Mon Sep 17 00:00:00 2001 From: mlboy Date: Wed, 12 Dec 2018 11:59:37 +0800 Subject: [PATCH 128/237] change: response_type as attribute --- oauthlib/oauth2/rfc6749/clients/mobile_application.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py index aa20daab..11c6c51c 100644 --- a/oauthlib/oauth2/rfc6749/clients/mobile_application.py +++ b/oauthlib/oauth2/rfc6749/clients/mobile_application.py @@ -45,6 +45,8 @@ class MobileApplicationClient(Client): redirection URI, it may be exposed to the resource owner and other applications residing on the same device. """ + + response_type = 'token' def prepare_request_uri(self, uri, redirect_uri=None, scope=None, state=None, **kwargs): @@ -91,7 +93,7 @@ def prepare_request_uri(self, uri, redirect_uri=None, scope=None, .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12 """ - return prepare_grant_uri(uri, self.client_id, 'token', + return prepare_grant_uri(uri, self.client_id, self.response_type, redirect_uri=redirect_uri, state=state, scope=scope, **kwargs) def parse_request_uri_response(self, uri, state=None, scope=None): From 5f629b5dce3fc6aafb5908480ed241c6f5b4cfbb Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 12 Dec 2018 17:58:45 +0100 Subject: [PATCH 129/237] Used WWW-Authenticate and auth-param values as RFC6750 described it. It misses the possibility to add scope= and realm= at the moment, but it should be a step forward into the right direction. --- .../oauth2/rfc6749/endpoints/introspect.py | 4 +--- .../oauth2/rfc6749/endpoints/revocation.py | 4 +--- oauthlib/oauth2/rfc6749/errors.py | 21 +++++++++++++++++++ .../rfc6749/grant_types/authorization_code.py | 3 +-- .../rfc6749/grant_types/client_credentials.py | 3 +-- .../rfc6749/grant_types/refresh_token.py | 3 +-- .../resource_owner_password_credentials.py | 3 +-- .../endpoints/test_introspect_endpoint.py | 4 ++-- .../endpoints/test_revocation_endpoint.py | 4 ++-- 9 files changed, 31 insertions(+), 18 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py index ac2e3286..4db1bdc3 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/introspect.py +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -63,9 +63,7 @@ def create_introspect_response(self, uri, http_method='POST', body=None, log.debug('Token introspect valid for %r.', request) except OAuth2Error as e: log.debug('Client error during validation of %r. %r.', request, e) - if e.status_code == 401: - return {"WWW-Authenticate": "Basic"}, e.json, e.status_code - return {}, e.json, e.status_code + return e.headers, e.json, e.status_code claims = self.request_validator.introspect_token( request.token, diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py index b13faa38..6c59a1e5 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py +++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py @@ -69,9 +69,7 @@ def create_revocation_response(self, uri, http_method='POST', body=None, response_body = e.json if self.enable_jsonp and request.callback: response_body = '%s(%s);' % (request.callback, response_body) - if e.status_code == 401: - return {"WWW-Authenticate": "Basic"}, response_body, e.status_code - return {}, response_body, e.status_code + return e.headers, response_body, e.status_code self.request_validator.revoke_token(request.token, request.token_type_hint, request) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index addcb6d2..e5543b5e 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -96,6 +96,27 @@ def urlencoded(self): def json(self): return json.dumps(dict(self.twotuples)) + @property + def headers(self): + if self.status_code == 401: + """ + https://tools.ietf.org/html/rfc6750#section-3 + + All challenges defined by this specification MUST use the auth-scheme + value "Bearer". This scheme MUST be followed by one or more + auth-param values. + """ + authvalues = [ + "Bearer", + "error={}".format(self.error) + ] + if self.description: + authvalues.append("error_description={}".format(self.description)) + if self.uri: + authvalues.append("error_uri={}".format(self.uri)) + return {"WWW-Authenticate": ", ".join(authvalues)} + return {} + class TokenExpiredError(OAuth2Error): error = 'token_expired' diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 334ed564..850d70a1 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -243,8 +243,7 @@ def create_token_response(self, request, token_handler): log.debug('Token request validation ok for %r.', request) except errors.OAuth2Error as e: log.debug('Client error during validation of %r. %r.', request, e) - if e.status_code == 401: - headers.update({"WWW-Authenticate": "Basic"}) + headers.update(e.headers) return headers, e.json, e.status_code token = token_handler.create_token(request, refresh_token=self.refresh_token, save_token=False) diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py index 54dbebc5..0e4f5457 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py @@ -77,8 +77,7 @@ def create_token_response(self, request, token_handler): self.validate_token_request(request) except errors.OAuth2Error as e: log.debug('Client error in token request. %s.', e) - if e.status_code == 401: - headers.update({"WWW-Authenticate": "Basic"}) + headers.update(e.headers) return headers, e.json, e.status_code token = token_handler.create_token(request, refresh_token=False, save_token=False) diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py index d2b3f6f2..67d65a77 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -64,8 +64,7 @@ def create_token_response(self, request, token_handler): self.validate_token_request(request) except errors.OAuth2Error as e: log.debug('Client error in token request, %s.', e) - if e.status_code == 401: - headers.update({"WWW-Authenticate": "Basic"}) + headers.update(e.headers) return headers, e.json, e.status_code token = token_handler.create_token(request, diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py index 931d76c8..cb5a4cac 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py @@ -105,8 +105,7 @@ def create_token_response(self, request, token_handler): self.validate_token_request(request) except errors.OAuth2Error as e: log.debug('Client error in token request, %s.', e) - if e.status_code == 401: - headers.update({"WWW-Authenticate": "Basic"}) + headers.update(e.headers) return headers, e.json, e.status_code token = token_handler.create_token(request, self.refresh_token, save_token=False) diff --git a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py index f7c80338..d252a735 100644 --- a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py @@ -86,7 +86,7 @@ def test_introspect_token_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_introspect_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {"WWW-Authenticate": "Basic"}) + self.assertEqual(h, {"WWW-Authenticate": "Bearer, error=invalid_client"}) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) @@ -109,7 +109,7 @@ def test_introspect_token_public_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_introspect_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {"WWW-Authenticate": "Basic"}) + self.assertEqual(h, {"WWW-Authenticate": "Bearer, error=invalid_client"}) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) diff --git a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py index db562c86..8a434e23 100644 --- a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py @@ -49,7 +49,7 @@ def test_revoke_token_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_revocation_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {"WWW-Authenticate": "Basic"}) + self.assertEqual(h, {"WWW-Authenticate": "Bearer, error=invalid_client"}) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) @@ -72,7 +72,7 @@ def test_revoke_token_public_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_revocation_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {"WWW-Authenticate": "Basic"}) + self.assertEqual(h, {"WWW-Authenticate": "Bearer, error=invalid_client"}) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) From a9ec83a40477e6b5b460b6f203607199f5f16779 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 12 Dec 2018 18:08:09 +0100 Subject: [PATCH 130/237] Add double-quotes to the key/values in WWW-Authenticate --- oauthlib/oauth2/rfc6749/errors.py | 6 +++--- tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py | 4 ++-- tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py index e5543b5e..ec2b0d17 100644 --- a/oauthlib/oauth2/rfc6749/errors.py +++ b/oauthlib/oauth2/rfc6749/errors.py @@ -108,12 +108,12 @@ def headers(self): """ authvalues = [ "Bearer", - "error={}".format(self.error) + 'error="{}"'.format(self.error) ] if self.description: - authvalues.append("error_description={}".format(self.description)) + authvalues.append('error_description="{}"'.format(self.description)) if self.uri: - authvalues.append("error_uri={}".format(self.uri)) + authvalues.append('error_uri="{}"'.format(self.uri)) return {"WWW-Authenticate": ", ".join(authvalues)} return {} diff --git a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py index d252a735..e41b83fd 100644 --- a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py @@ -86,7 +86,7 @@ def test_introspect_token_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_introspect_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {"WWW-Authenticate": "Bearer, error=invalid_client"}) + self.assertEqual(h, {"WWW-Authenticate": 'Bearer, error="invalid_client"'}) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) @@ -109,7 +109,7 @@ def test_introspect_token_public_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_introspect_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {"WWW-Authenticate": "Bearer, error=invalid_client"}) + self.assertEqual(h, {"WWW-Authenticate": 'Bearer, error="invalid_client"'}) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) diff --git a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py index 8a434e23..a6a5cb2f 100644 --- a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py @@ -49,7 +49,7 @@ def test_revoke_token_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_revocation_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {"WWW-Authenticate": "Bearer, error=invalid_client"}) + self.assertEqual(h, {"WWW-Authenticate": 'Bearer, error="invalid_client"'}) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) @@ -72,7 +72,7 @@ def test_revoke_token_public_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_revocation_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {"WWW-Authenticate": "Bearer, error=invalid_client"}) + self.assertEqual(h, {"WWW-Authenticate": 'Bearer, error="invalid_client"'}) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) From 61458583d83959a37e56c20eb09546aaa63b4829 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 13 Dec 2018 10:43:12 +0100 Subject: [PATCH 131/237] Add Content-Type and Cache headers to introspect/revocation errors --- .../oauth2/rfc6749/endpoints/introspect.py | 13 ++++++----- .../oauth2/rfc6749/endpoints/revocation.py | 8 ++++++- .../endpoints/test_introspect_endpoint.py | 18 +++++++++++---- .../endpoints/test_revocation_endpoint.py | 23 +++++++++++++++---- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py index 4db1bdc3..4a531e44 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/introspect.py +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -57,24 +57,25 @@ def create_introspect_response(self, uri, http_method='POST', body=None, an introspection response indicating the token is not active as described in Section 2.2. """ + headers = { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } request = Request(uri, http_method, body, headers) try: self.validate_introspect_request(request) log.debug('Token introspect valid for %r.', request) except OAuth2Error as e: log.debug('Client error during validation of %r. %r.', request, e) - return e.headers, e.json, e.status_code + headers.update(e.headers) + return headers, e.json, e.status_code claims = self.request_validator.introspect_token( request.token, request.token_type_hint, request ) - headers = { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store', - 'Pragma': 'no-cache', - } if claims is None: return headers, json.dumps(dict(active=False)), 200 if "active" in claims: diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py index 6c59a1e5..f7e591d5 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py +++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py @@ -59,6 +59,11 @@ def create_revocation_response(self, uri, http_method='POST', body=None, An invalid token type hint value is ignored by the authorization server and does not influence the revocation response. """ + headers = { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } request = Request( uri, http_method=http_method, body=body, headers=headers) try: @@ -69,7 +74,8 @@ def create_revocation_response(self, uri, http_method='POST', body=None, response_body = e.json if self.enable_jsonp and request.callback: response_body = '%s(%s);' % (request.callback, response_body) - return e.headers, response_body, e.status_code + headers.update(e.headers) + return headers, response_body, e.status_code self.request_validator.revoke_token(request.token, request.token_type_hint, request) diff --git a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py index e41b83fd..f92652b8 100644 --- a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py @@ -86,7 +86,12 @@ def test_introspect_token_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_introspect_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {"WWW-Authenticate": 'Bearer, error="invalid_client"'}) + self.assertEqual(h, { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + "WWW-Authenticate": 'Bearer, error="invalid_client"' + }) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) @@ -109,7 +114,12 @@ def test_introspect_token_public_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_introspect_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {"WWW-Authenticate": 'Bearer, error="invalid_client"'}) + self.assertEqual(h, { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + "WWW-Authenticate": 'Bearer, error="invalid_client"' + }) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) @@ -121,12 +131,12 @@ def test_introspect_unsupported_token(self): ('token_type_hint', 'refresh_token')]) h, b, s = endpoint.create_introspect_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {}) + self.assertEqual(h, self.resp_h) self.assertEqual(loads(b)['error'], 'unsupported_token_type') self.assertEqual(s, 400) h, b, s = endpoint.create_introspect_response(self.uri, headers=self.headers, body='') - self.assertEqual(h, {}) + self.assertEqual(h, self.resp_h) self.assertEqual(loads(b)['error'], 'invalid_request') 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 a6a5cb2f..2a241770 100644 --- a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py @@ -24,6 +24,11 @@ def setUp(self): self.headers = { 'Content-Type': 'application/x-www-form-urlencoded', } + self.resp_h = { + 'Cache-Control': 'no-store', + 'Content-Type': 'application/json', + 'Pragma': 'no-cache' + } def test_revoke_token(self): for token_type in ('access_token', 'refresh_token', 'invalid'): @@ -49,7 +54,12 @@ def test_revoke_token_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_revocation_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {"WWW-Authenticate": 'Bearer, error="invalid_client"'}) + self.assertEqual(h, { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + "WWW-Authenticate": 'Bearer, error="invalid_client"' + }) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) @@ -72,7 +82,12 @@ def test_revoke_token_public_client_authentication_failed(self): ('token_type_hint', 'access_token')]) h, b, s = self.endpoint.create_revocation_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {"WWW-Authenticate": 'Bearer, error="invalid_client"'}) + self.assertEqual(h, { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + "WWW-Authenticate": 'Bearer, error="invalid_client"' + }) self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) @@ -96,12 +111,12 @@ def test_revoke_unsupported_token(self): ('token_type_hint', 'refresh_token')]) h, b, s = endpoint.create_revocation_response(self.uri, headers=self.headers, body=body) - self.assertEqual(h, {}) + self.assertEqual(h, self.resp_h) self.assertEqual(loads(b)['error'], 'unsupported_token_type') self.assertEqual(s, 400) h, b, s = endpoint.create_revocation_response(self.uri, headers=self.headers, body='') - self.assertEqual(h, {}) + self.assertEqual(h, self.resp_h) self.assertEqual(loads(b)['error'], 'invalid_request') self.assertEqual(s, 400) From d0640038a84f60b37864cf585fd764f9ee34b1c4 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 13 Dec 2018 11:00:15 +0100 Subject: [PATCH 132/237] challenge can have a length of 128 when using maximum size of verifier+plain. --- docs/oauth2/server.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/oauth2/server.rst b/docs/oauth2/server.rst index eca363b7..6c065c57 100644 --- a/docs/oauth2/server.rst +++ b/docs/oauth2/server.rst @@ -253,7 +253,7 @@ the token. .. code-block:: python - challenge = django.db.models.CharField(max_length=100) + challenge = django.db.models.CharField(max_length=128) challenge_method = django.db.models.CharField(max_length=6) From 1a7be4eebb11cd5224c3b6eaf1782e8add5bd8d9 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 13 Dec 2018 16:05:29 +0100 Subject: [PATCH 133/237] Replace temporary list by using clearer "extend" method --- oauthlib/oauth2/rfc6749/endpoints/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py index 84ddf8f8..fe6545f3 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/metadata.py +++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py @@ -89,7 +89,7 @@ def validate_metadata(self, array, key, is_required=False, is_list=False, is_url raise ValueError("array {}: {} must contains only string (not {})".format(key, array[key], elem)) def validate_metadata_token(self, claims, endpoint): - self._grant_types += list(endpoint._grant_types.keys()) + self._grant_types.extend(endpoint._grant_types.keys()) claims.setdefault("token_endpoint_auth_methods_supported", ["client_secret_post", "client_secret_basic"]) self.validate_metadata(claims, "token_endpoint_auth_methods_supported", is_list=True) From 6dcde73a81d6cbc718ca9ca7f9170a28fc1b5e34 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 13 Dec 2018 16:31:03 +0100 Subject: [PATCH 134/237] Add details on grant_type & implicit special case. --- oauthlib/oauth2/rfc6749/endpoints/metadata.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py index fe6545f3..c2d59186 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/metadata.py +++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py @@ -89,6 +89,12 @@ def validate_metadata(self, array, key, is_required=False, is_list=False, is_url raise ValueError("array {}: {} must contains only string (not {})".format(key, array[key], elem)) def validate_metadata_token(self, claims, endpoint): + """ + If the token endpoint is used in the grant type, the value of this + parameter MUST be the same as the value of the "grant_type" + parameter passed to the token endpoint defined in the grant type + definition. + """ self._grant_types.extend(endpoint._grant_types.keys()) claims.setdefault("token_endpoint_auth_methods_supported", ["client_secret_post", "client_secret_basic"]) @@ -100,6 +106,10 @@ def validate_metadata_authorization(self, claims, endpoint): claims.setdefault("response_types_supported", list(filter(lambda x: x != "none", endpoint._response_types.keys()))) claims.setdefault("response_modes_supported", ["query", "fragment"]) + + # The OAuth2.0 Implicit flow is defined as a "grant type" but it is not + # using the "token" endpoint, at such, we have to add it explicitly to + # the list of "grant_types_supported" when enabled. if "token" in claims["response_types_supported"]: self._grant_types.append("implicit") @@ -196,6 +206,8 @@ def validate_metadata_server(self): if isinstance(endpoint, IntrospectEndpoint): self.validate_metadata_introspection(claims, endpoint) + # "grant_types_supported" is a combination of all OAuth2 grant types + # allowed in the current provider implementation. claims.setdefault("grant_types_supported", self._grant_types) self.validate_metadata(claims, "grant_types_supported", is_list=True) return claims From 7a0b1e9cad04806bf4be5c7380e75aaf03ebec2c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 13 Dec 2018 16:32:00 +0100 Subject: [PATCH 135/237] Add OAuth2.0 Authorization Server Metadata documentation --- docs/oauth2/endpoints/endpoints.rst | 6 ++- oauthlib/oauth2/rfc6749/endpoints/metadata.py | 42 +++++++++++++++---- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/docs/oauth2/endpoints/endpoints.rst b/docs/oauth2/endpoints/endpoints.rst index 8068ec46..0dd2da0f 100644 --- a/docs/oauth2/endpoints/endpoints.rst +++ b/docs/oauth2/endpoints/endpoints.rst @@ -10,12 +10,14 @@ certain users resources to a client, to supply said client with a token embodying this authorization and to verify that the token is valid when the client attempts to access the user resources on their behalf. + .. toctree:: :maxdepth: 2 authorization introspect token + metadata revocation resource @@ -29,5 +31,5 @@ later (but it's applicable to all other web frameworks libraries). The main purpose of the endpoint in OAuthLib is to figure out which grant type or token to dispatch the request to. -Then, you can extend your OAuth implementation by proposing introspect or -revocation endpoints. +Then, you can extend your OAuth implementation by proposing introspect, +revocation and/or providing metadata endpoints. diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py index 6d77b9f5..ad56c425 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/metadata.py +++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py @@ -32,14 +32,13 @@ class MetadataEndpoint(BaseEndpoint): `OpenID Connect Discovery 1.0` in a way that is compatible with OpenID Connect Discovery while being applicable to a wider set of OAuth 2.0 use cases. This is intentionally parallel to the way - that `OAuth 2.0 Dynamic Client Registration Protocol` [RFC7591] + that OAuth 2.0 Dynamic Client Registration Protocol [`RFC7591`_] generalized the dynamic client registration mechanisms defined by - `OpenID Connect Dynamic Client Registration 1.0` + OpenID Connect Dynamic Client Registration 1.0 in a way that is compatible with it. - .. _`OpenID Connect Discovery 1.0`: http://openid.net/specs/openid-connect-discovery-1_0.html - .. _`OAuth 2.0 Dynamic Client Registration Protocol`: https://tools.ietf.org/html/rfc7591 - .. _`OpenID Connect Dynamic Client Registration 1.0`: https://openid.net/specs/openid-connect-registration-1_0.html + .. _`OpenID Connect Discovery 1.0`: https://openid.net/specs/openid-connect-discovery-1_0.html + .. _`RFC7591`: https://tools.ietf.org/html/rfc7591 """ def __init__(self, endpoints, claims={}, raise_errors=True): @@ -127,18 +126,19 @@ def validate_metadata_server(self): """ Authorization servers can have metadata describing their configuration. The following authorization server metadata values - are used by this specification. More details can be found in `RFC8414` : + are used by this specification. More details can be found in + `RFC8414 section 2`_ : issuer REQUIRED authorization_endpoint URL of the authorization server's authorization endpoint - [RFC6749]. This is REQUIRED unless no grant types are supported + [`RFC6749#Authorization`_]. This is REQUIRED unless no grant types are supported that use the authorization endpoint. token_endpoint - URL of the authorization server's token endpoint [RFC6749]. This + URL of the authorization server's token endpoint [`RFC6749#Token`_]. This is REQUIRED unless only the implicit grant type is supported. scopes_supported @@ -151,26 +151,50 @@ def validate_metadata_server(self): jwks_uri registration_endpoint response_modes_supported + grant_types_supported + OPTIONAL. JSON array containing a list of the OAuth 2.0 grant + type values that this authorization server supports. The array + values used are the same as those used with the "grant_types" + parameter defined by "OAuth 2.0 Dynamic Client Registration + Protocol" [`RFC7591`_]. If omitted, the default value is + "["authorization_code", "implicit"]". + token_endpoint_auth_methods_supported + token_endpoint_auth_signing_alg_values_supported + service_documentation + ui_locales_supported + op_policy_uri + op_tos_uri + revocation_endpoint + revocation_endpoint_auth_methods_supported + revocation_endpoint_auth_signing_alg_values_supported + introspection_endpoint + introspection_endpoint_auth_methods_supported + introspection_endpoint_auth_signing_alg_values_supported + code_challenge_methods_supported Additional authorization server metadata parameters MAY also be used. Some are defined by other specifications, such as OpenID Connect - Discovery 1.0 [OpenID.Discovery]. + Discovery 1.0 [`OpenID.Discovery`_]. .. _`RFC8414 section 2`: https://tools.ietf.org/html/rfc8414#section-2 + .. _`RFC6749#Authorization`: https://tools.ietf.org/html/rfc6749#section-3.1 + .. _`RFC6749#Token`: https://tools.ietf.org/html/rfc6749#section-3.2 + .. _`RFC7591`: https://tools.ietf.org/html/rfc7591 + .. _`OpenID.Discovery`: https://openid.net/specs/openid-connect-discovery-1_0.html """ claims = copy.deepcopy(self.initial_claims) self.validate_metadata(claims, "issuer", is_required=True, is_issuer=True) From bc53c6189a1096fd1f112be42f372d70465ab4ac Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 13 Dec 2018 17:15:18 +0100 Subject: [PATCH 136/237] Add metadata documentation with quick example --- docs/oauth2/endpoints/metadata.rst | 72 ++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/oauth2/endpoints/metadata.rst diff --git a/docs/oauth2/endpoints/metadata.rst b/docs/oauth2/endpoints/metadata.rst new file mode 100644 index 00000000..d44e8b7b --- /dev/null +++ b/docs/oauth2/endpoints/metadata.rst @@ -0,0 +1,72 @@ +=================== +Metadata endpoint +=================== + +OAuth2.0 Authorization Server Metadata (`RFC8414`_) endpoint provide the metadata of your authorization server. Since the metadata results can be a combination of OAuthlib's Endpoint (see :doc:`preconfigured_servers`), the MetadataEndpoint's class takes a list of Endpoints in parameter, and aggregate the metadata in the response. + +See below an example of usage with `bottle-oauthlib`_ when using a `LegacyApplicationServer` (password grant) endpoint: + +.. code-block:: python + + import bottle + from bottle_oauthlib.oauth2 import BottleOAuth2 + from oauthlib import oauth2 + + app = bottle.Bottle() + app.authmetadata = BottleOAuth2(app) + + oauthlib_server = oauth2.LegacyApplicationServer(oauth2.RequestValidator()) + app.authmetadata.initialize(oauth2.MetadataEndpoint([oauthlib_server], claims={ + "issuer": "https://xx", + "token_endpoint": "https://xx/token", + "revocation_endpoint": "https://xx/revoke", + "introspection_endpoint": "https://xx/tokeninfo" + })) + + + @app.get('/.well-known/oauth-authorization-server') + @app.authmetadata.create_metadata_response() + def metadata(): + pass + + + if __name__ == "__main__": + app.run() # pragma: no cover + + +Sample response's output: + + +.. code-block:: javascript + + $ curl -s http://localhost:8080/.well-known/oauth-authorization-server|jq . + { + "issuer": "https://xx", + "token_endpoint": "https://xx/token", + "revocation_endpoint": "https://xx/revoke", + "introspection_endpoint": "https://xx/tokeninfo", + "grant_types_supported": [ + "password", + "refresh_token" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_post", + "client_secret_basic" + ], + "revocation_endpoint_auth_methods_supported": [ + "client_secret_post", + "client_secret_basic" + ], + "introspection_endpoint_auth_methods_supported": [ + "client_secret_post", + "client_secret_basic" + ] + } + + +.. autoclass:: oauthlib.oauth2.MetadataEndpoint + :members: + + +.. _`RFC8414`: https://tools.ietf.org/html/rfc8414 +.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthli From ac23d0973b441cd2afdcabe24f474147eada8242 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 13 Dec 2018 17:16:23 +0100 Subject: [PATCH 137/237] Fixed typo --- oauthlib/oauth2/rfc6749/endpoints/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py index c2d59186..45cf1103 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/metadata.py +++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py @@ -108,7 +108,7 @@ def validate_metadata_authorization(self, claims, endpoint): claims.setdefault("response_modes_supported", ["query", "fragment"]) # The OAuth2.0 Implicit flow is defined as a "grant type" but it is not - # using the "token" endpoint, at such, we have to add it explicitly to + # using the "token" endpoint, as such, we have to add it explicitly to # the list of "grant_types_supported" when enabled. if "token" in claims["response_types_supported"]: self._grant_types.append("implicit") From 8aca902011981a236cedf32d0c859078c7881b71 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Thu, 13 Dec 2018 18:29:50 +0200 Subject: [PATCH 138/237] Update comments regarding OAuth Request Body Hash. (#628) --- oauthlib/oauth1/rfc5849/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oauthlib/oauth1/rfc5849/__init__.py b/oauthlib/oauth1/rfc5849/__init__.py index 887ab69f..7313286c 100644 --- a/oauthlib/oauth1/rfc5849/__init__.py +++ b/oauthlib/oauth1/rfc5849/__init__.py @@ -173,10 +173,12 @@ def get_oauth_params(self, request): params.append(('oauth_verifier', self.verifier)) # providing body hash for requests other than x-www-form-urlencoded - # as described in http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html + # as described in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-4.1.1 # 4.1.1. When to include the body hash # * [...] MUST NOT include an oauth_body_hash parameter on requests with form-encoded request bodies # * [...] SHOULD include the oauth_body_hash parameter on all other requests. + # Note that SHA-1 is vulnerable. The spec acknowledges that in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-6.2 + # At this time, no further effort has been made to replace SHA-1 for the OAuth Request Body Hash extension. content_type = request.headers.get('Content-Type', None) content_type_eligible = content_type and content_type.find('application/x-www-form-urlencoded') < 0 if request.body is not None and content_type_eligible: From 7bd82f55bc176e368b6d0b88c9a216f37b8fe753 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 14 Dec 2018 13:05:50 +0100 Subject: [PATCH 139/237] Fix issue when using Metadata Endpoint with OIDC PreConfigured server. --- oauthlib/oauth2/rfc6749/endpoints/metadata.py | 2 +- .../connect/core/endpoints/pre_configured.py | 6 +++-- .../oauth2/rfc6749/endpoints/test_metadata.py | 27 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py index 6d77b9f5..efa33e07 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/metadata.py +++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py @@ -98,7 +98,7 @@ def validate_metadata_token(self, claims, endpoint): self.validate_metadata(claims, "token_endpoint", is_required=True, is_url=True) def validate_metadata_authorization(self, claims, endpoint): - claims.setdefault("response_types_supported", list(self._response_types.keys())) + claims.setdefault("response_types_supported", list(endpoint._response_types.keys())) claims.setdefault("response_modes_supported", ["query", "fragment"]) self.validate_metadata(claims, "response_types_supported", is_required=True, is_list=True) diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 9cf30db4..6367847b 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -10,6 +10,7 @@ from oauthlib.oauth2.rfc6749.endpoints import ( AuthorizationEndpoint, + IntrospectEndpoint, ResourceEndpoint, RevocationEndpoint, TokenEndpoint @@ -35,8 +36,8 @@ from ..tokens import JWTToken -class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring all four major grant types.""" @@ -103,3 +104,4 @@ def __init__(self, request_validator, token_expires_in=None, ResourceEndpoint.__init__(self, default_token='Bearer', token_types={'Bearer': bearer, 'JWT': jwt}) RevocationEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator) diff --git a/tests/oauth2/rfc6749/endpoints/test_metadata.py b/tests/oauth2/rfc6749/endpoints/test_metadata.py index 301e8469..7108d104 100644 --- a/tests/oauth2/rfc6749/endpoints/test_metadata.py +++ b/tests/oauth2/rfc6749/endpoints/test_metadata.py @@ -13,6 +13,33 @@ def setUp(self): "issuer": 'https://foo.bar' } + def test_openid_oauth2_preconfigured(self): + default_claims = { + "issuer": 'https://foo.bar', + "authorization_endpoint": "https://foo.bar/authorize", + "revocation_endpoint": "https://foo.bar/revoke", + "introspection_endpoint": "https://foo.bar/introspect", + "token_endpoint": "https://foo.bar/token" + } + from oauthlib.oauth2 import Server as OAuth2Server + from oauthlib.openid import Server as OpenIDServer + + endpoint = OAuth2Server(None) + metadata = MetadataEndpoint([endpoint], default_claims) + oauth2_claims = metadata.claims + + endpoint = OpenIDServer(None) + metadata = MetadataEndpoint([endpoint], default_claims) + openid_claims = metadata.claims + + # Pure OAuth2 Authorization Metadata are similar with OpenID but + # response_type not! (OIDC contains "id_token" and hybrid flows) + del oauth2_claims['response_types_supported'] + del openid_claims['response_types_supported'] + + self.maxDiff = None + self.assertEqual(openid_claims, oauth2_claims) + def test_token_endpoint(self): endpoint = TokenEndpoint(None, None, grant_types={"password": None}) metadata = MetadataEndpoint([endpoint], { From 7be2769bcfefc5db9b54603dbbbcd4db0d237216 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 14 Dec 2018 13:05:50 +0100 Subject: [PATCH 140/237] Fix issue when using Metadata Endpoint with OIDC PreConfigured server. --- .../connect/core/endpoints/pre_configured.py | 6 +++-- .../oauth2/rfc6749/endpoints/test_metadata.py | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 9cf30db4..6367847b 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -10,6 +10,7 @@ from oauthlib.oauth2.rfc6749.endpoints import ( AuthorizationEndpoint, + IntrospectEndpoint, ResourceEndpoint, RevocationEndpoint, TokenEndpoint @@ -35,8 +36,8 @@ from ..tokens import JWTToken -class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint, - RevocationEndpoint): +class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, + ResourceEndpoint, RevocationEndpoint): """An all-in-one endpoint featuring all four major grant types.""" @@ -103,3 +104,4 @@ def __init__(self, request_validator, token_expires_in=None, ResourceEndpoint.__init__(self, default_token='Bearer', token_types={'Bearer': bearer, 'JWT': jwt}) RevocationEndpoint.__init__(self, request_validator) + IntrospectEndpoint.__init__(self, request_validator) diff --git a/tests/oauth2/rfc6749/endpoints/test_metadata.py b/tests/oauth2/rfc6749/endpoints/test_metadata.py index 875316ae..4813b46e 100644 --- a/tests/oauth2/rfc6749/endpoints/test_metadata.py +++ b/tests/oauth2/rfc6749/endpoints/test_metadata.py @@ -14,6 +14,33 @@ def setUp(self): "issuer": 'https://foo.bar' } + def test_openid_oauth2_preconfigured(self): + default_claims = { + "issuer": 'https://foo.bar', + "authorization_endpoint": "https://foo.bar/authorize", + "revocation_endpoint": "https://foo.bar/revoke", + "introspection_endpoint": "https://foo.bar/introspect", + "token_endpoint": "https://foo.bar/token" + } + from oauthlib.oauth2 import Server as OAuth2Server + from oauthlib.openid import Server as OpenIDServer + + endpoint = OAuth2Server(None) + metadata = MetadataEndpoint([endpoint], default_claims) + oauth2_claims = metadata.claims + + endpoint = OpenIDServer(None) + metadata = MetadataEndpoint([endpoint], default_claims) + openid_claims = metadata.claims + + # Pure OAuth2 Authorization Metadata are similar with OpenID but + # response_type not! (OIDC contains "id_token" and hybrid flows) + del oauth2_claims['response_types_supported'] + del openid_claims['response_types_supported'] + + self.maxDiff = None + self.assertEqual(openid_claims, oauth2_claims) + def test_token_endpoint(self): endpoint = TokenEndpoint(None, None, grant_types={"password": None}) metadata = MetadataEndpoint([endpoint], { From f6b6d1460755c31d522e5e01be28bfd5b1f9da33 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 14 Dec 2018 13:25:43 +0100 Subject: [PATCH 141/237] Fixed OAuth2 Metadata when using PKCE and OIDC.Server --- oauthlib/oauth2/rfc6749/endpoints/metadata.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py index 45cf1103..60c846b0 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/metadata.py +++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py @@ -19,6 +19,7 @@ from .introspect import IntrospectEndpoint from .token import TokenEndpoint from .revocation import RevocationEndpoint +from .. import grant_types log = logging.getLogger(__name__) @@ -116,8 +117,12 @@ def validate_metadata_authorization(self, claims, endpoint): self.validate_metadata(claims, "response_types_supported", is_required=True, is_list=True) self.validate_metadata(claims, "response_modes_supported", is_list=True) if "code" in claims["response_types_supported"]: + code_grant = endpoint._response_types["code"] + if not isinstance(code_grant, grant_types.AuthorizationCodeGrant) and hasattr(code_grant, "default_grant"): + code_grant = code_grant.default_grant + claims.setdefault("code_challenge_methods_supported", - list(endpoint._response_types["code"]._code_challenge_methods.keys())) + list(code_grant._code_challenge_methods.keys())) self.validate_metadata(claims, "code_challenge_methods_supported", is_list=True) self.validate_metadata(claims, "authorization_endpoint", is_required=True, is_url=True) From 4bd39a770ce48d94ab8914463e20e9002e0b4869 Mon Sep 17 00:00:00 2001 From: Florent Captier Date: Sun, 16 Dec 2018 14:30:51 +0100 Subject: [PATCH 142/237] Use pytest as test framework Closes #631 --- docs/contributing.rst | 4 ++-- requirements-test.txt | 3 ++- tox.ini | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 771262d9..e101f702 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -144,7 +144,7 @@ the project root via: .. sourcecode:: bash - $ python -m unittest discover + $ py.test The first thing the core committers will do is run this command. Any pull request that fails this test suite will be **rejected**. @@ -301,7 +301,7 @@ First we pull the code into a local branch:: Then we run the tests:: - python -m unittest discover + py.test We finish with a non-fastforward merge (to preserve the branch history) and push to GitHub:: diff --git a/requirements-test.txt b/requirements-test.txt index c3e0a7ba..64485a62 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,3 +1,4 @@ -r requirements.txt -coverage>=3.7.1 mock>=2.0 +pytest>=4.0 +pytest-cov>=2.6 diff --git a/tox.ini b/tox.ini index 47237d8c..05f12a69 100644 --- a/tox.ini +++ b/tox.ini @@ -5,8 +5,7 @@ envlist = py27,py34,py35,py36,py37,pypy,pypy3,docs,readme deps= -rrequirements-test.txt commands= - coverage run --source oauthlib -m unittest discover - coverage report + py.test --cov=oauthlib tests/ # tox -e docs to mimick readthedocs build. From 9faf472795c49008cc9b727b865b3a13d72ede50 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 17 Dec 2018 14:53:00 +0200 Subject: [PATCH 143/237] Extract default grant headers to helper method. --- oauthlib/oauth2/rfc6749/grant_types/authorization_code.py | 8 ++------ oauthlib/oauth2/rfc6749/grant_types/base.py | 8 ++++++++ oauthlib/oauth2/rfc6749/grant_types/client_credentials.py | 6 +----- oauthlib/oauth2/rfc6749/grant_types/refresh_token.py | 6 +----- .../grant_types/resource_owner_password_credentials.py | 6 +----- 5 files changed, 13 insertions(+), 21 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 8ebae498..355ea1b0 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -100,7 +100,7 @@ class AuthorizationCodeGrant(GrantTypeBase): def create_authorization_code(self, request): """ Generates an authorization grant represented as a dictionary. - + :param request: OAuthlib request. :type request: oauthlib.common.Request """ @@ -233,11 +233,7 @@ def create_token_response(self, request, token_handler): oauthlib.oauth2.BearerToken. """ - headers = { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store', - 'Pragma': 'no-cache', - } + headers = self._get_default_headers() try: self.validate_token_request(request) log.debug('Token request validation ok for %r.', request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/base.py b/oauthlib/oauth2/rfc6749/grant_types/base.py index 43f9db40..6ca8f65b 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/base.py +++ b/oauthlib/oauth2/rfc6749/grant_types/base.py @@ -216,3 +216,11 @@ def prepare_authorization_response(self, request, token, headers, body, status): raise NotImplementedError( 'Subclasses must set a valid default_response_mode') + + def _get_default_headers(self): + """Create default headers for grant responses.""" + return { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py index 7d4f74cb..5e8fdc0a 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py @@ -67,11 +67,7 @@ def create_token_response(self, request, token_handler): .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1 .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2 """ - headers = { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store', - 'Pragma': 'no-cache', - } + headers = self._get_default_headers() try: log.debug('Validating access token request, %r.', request) self.validate_token_request(request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py index 5f7382a7..78963c30 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -54,11 +54,7 @@ def create_token_response(self, request, token_handler): .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1 .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2 """ - headers = { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store', - 'Pragma': 'no-cache', - } + headers = self._get_default_headers() try: log.debug('Validating refresh token request, %r.', request) self.validate_token_request(request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py index 87e80152..95082afc 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py @@ -87,11 +87,7 @@ def create_token_response(self, request, token_handler): .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1 .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2 """ - headers = { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store', - 'Pragma': 'no-cache', - } + headers = self._get_default_headers() try: if self.request_validator.client_authentication_required(request): log.debug('Authenticating client, %r.', request) From baeb737f60f848a58f247a6ca8cf8b44445efcc0 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 17 Dec 2018 15:03:00 +0200 Subject: [PATCH 144/237] Extract redirect handling to a common method. --- .../rfc6749/grant_types/authorization_code.py | 21 +---- oauthlib/oauth2/rfc6749/grant_types/base.py | 83 ++++++++++++------- .../oauth2/rfc6749/grant_types/implicit.py | 25 +----- 3 files changed, 59 insertions(+), 70 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 355ea1b0..0cbcb8cf 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -9,7 +9,6 @@ import logging from oauthlib import common -from oauthlib.uri_validate import is_absolute_uri from .. import errors from .base import GrantTypeBase @@ -295,24 +294,10 @@ def validate_authorization_request(self, request): # https://tools.ietf.org/html/rfc6749#section-3.1.2 log.debug('Validating redirection uri %s for client %s.', request.redirect_uri, request.client_id) - if request.redirect_uri is not None: - request.using_default_redirect_uri = False - log.debug('Using provided redirect_uri %s', request.redirect_uri) - if not is_absolute_uri(request.redirect_uri): - raise errors.InvalidRedirectURIError(request=request) - if not self.request_validator.validate_redirect_uri( - request.client_id, request.redirect_uri, request): - raise errors.MismatchingRedirectURIError(request=request) - else: - request.redirect_uri = self.request_validator.get_default_redirect_uri( - request.client_id, request) - request.using_default_redirect_uri = True - log.debug('Using default redirect_uri %s.', request.redirect_uri) - if not request.redirect_uri: - raise errors.MissingRedirectURIError(request=request) - if not is_absolute_uri(request.redirect_uri): - raise errors.InvalidRedirectURIError(request=request) + # OPTIONAL. As described in Section 3.1.2. + # https://tools.ietf.org/html/rfc6749#section-3.1.2 + self._handle_redirects(request) # Then check for normal errors. diff --git a/oauthlib/oauth2/rfc6749/grant_types/base.py b/oauthlib/oauth2/rfc6749/grant_types/base.py index 6ca8f65b..f0772e28 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/base.py +++ b/oauthlib/oauth2/rfc6749/grant_types/base.py @@ -9,51 +9,53 @@ from itertools import chain from oauthlib.common import add_params_to_uri +from oauthlib.uri_validate import is_absolute_uri from oauthlib.oauth2.rfc6749 import errors, utils from ..request_validator import RequestValidator log = logging.getLogger(__name__) + class ValidatorsContainer(object): """ - Container object for holding custom validator callables to be invoked - as part of the grant type `validate_authorization_request()` or - `validate_authorization_request()` methods on the various grant types. + Container object for holding custom validator callables to be invoked + as part of the grant type `validate_authorization_request()` or + `validate_authorization_request()` methods on the various grant types. - Authorization validators must be callables that take a request object and - return a dict, which may contain items to be added to the `request_info` - returned from the grant_type after validation. + Authorization validators must be callables that take a request object and + return a dict, which may contain items to be added to the `request_info` + returned from the grant_type after validation. - Token validators must be callables that take a request object and - return None. + Token validators must be callables that take a request object and + return None. - Both authorization validators and token validators may raise OAuth2 - exceptions if validation conditions fail. + Both authorization validators and token validators may raise OAuth2 + exceptions if validation conditions fail. - Authorization validators added to `pre_auth` will be run BEFORE - the standard validations (but after the critical ones that raise - fatal errors) as part of `validate_authorization_request()` + Authorization validators added to `pre_auth` will be run BEFORE + the standard validations (but after the critical ones that raise + fatal errors) as part of `validate_authorization_request()` - Authorization validators added to `post_auth` will be run AFTER - the standard validations as part of `validate_authorization_request()` + Authorization validators added to `post_auth` will be run AFTER + the standard validations as part of `validate_authorization_request()` - Token validators added to `pre_token` will be run BEFORE - the standard validations as part of `validate_token_request()` + Token validators added to `pre_token` will be run BEFORE + the standard validations as part of `validate_token_request()` - Token validators added to `post_token` will be run AFTER - the standard validations as part of `validate_token_request()` + Token validators added to `post_token` will be run AFTER + the standard validations as part of `validate_token_request()` - For example: + For example: - >>> def my_auth_validator(request): - ... return {'myval': True} - >>> auth_code_grant = AuthorizationCodeGrant(request_validator) - >>> auth_code_grant.custom_validators.pre_auth.append(my_auth_validator) - >>> def my_token_validator(request): - ... if not request.everything_okay: - ... raise errors.OAuth2Error("uh-oh") - >>> auth_code_grant.custom_validators.post_token.append(my_token_validator) + >>> def my_auth_validator(request): + ... return {'myval': True} + >>> auth_code_grant = AuthorizationCodeGrant(request_validator) + >>> auth_code_grant.custom_validators.pre_auth.append(my_auth_validator) + >>> def my_token_validator(request): + ... if not request.everything_okay: + ... raise errors.OAuth2Error("uh-oh") + >>> auth_code_grant.custom_validators.post_token.append(my_token_validator) """ def __init__(self, post_auth, post_token, @@ -224,3 +226,28 @@ def _get_default_headers(self): 'Cache-Control': 'no-store', 'Pragma': 'no-cache', } + + def _handle_redirects(self, request): + if request.redirect_uri is not None: + request.using_default_redirect_uri = False + log.debug('Using provided redirect_uri %s', request.redirect_uri) + if not is_absolute_uri(request.redirect_uri): + raise errors.InvalidRedirectURIError(request=request) + + # The authorization server MUST verify that the redirection URI + # to which it will redirect the access token matches a + # redirection URI registered by the client as described in + # Section 3.1.2. + # https://tools.ietf.org/html/rfc6749#section-3.1.2 + if not self.request_validator.validate_redirect_uri( + request.client_id, request.redirect_uri, request): + raise errors.MismatchingRedirectURIError(request=request) + else: + request.redirect_uri = self.request_validator.get_default_redirect_uri( + request.client_id, request) + request.using_default_redirect_uri = True + log.debug('Using default redirect_uri %s.', request.redirect_uri) + if not request.redirect_uri: + raise errors.MissingRedirectURIError(request=request) + if not is_absolute_uri(request.redirect_uri): + raise errors.InvalidRedirectURIError(request=request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py index b29953bf..d6de9061 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py +++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py @@ -8,7 +8,6 @@ import logging from oauthlib import common -from oauthlib.uri_validate import is_absolute_uri from .. import errors from .base import GrantTypeBase @@ -307,29 +306,7 @@ def validate_token_request(self, request): # OPTIONAL. As described in Section 3.1.2. # https://tools.ietf.org/html/rfc6749#section-3.1.2 - if request.redirect_uri is not None: - request.using_default_redirect_uri = False - log.debug('Using provided redirect_uri %s', request.redirect_uri) - if not is_absolute_uri(request.redirect_uri): - raise errors.InvalidRedirectURIError(request=request) - - # The authorization server MUST verify that the redirection URI - # to which it will redirect the access token matches a - # redirection URI registered by the client as described in - # Section 3.1.2. - # https://tools.ietf.org/html/rfc6749#section-3.1.2 - if not self.request_validator.validate_redirect_uri( - request.client_id, request.redirect_uri, request): - raise errors.MismatchingRedirectURIError(request=request) - else: - request.redirect_uri = self.request_validator.get_default_redirect_uri( - request.client_id, request) - request.using_default_redirect_uri = True - log.debug('Using default redirect_uri %s.', request.redirect_uri) - if not request.redirect_uri: - raise errors.MissingRedirectURIError(request=request) - if not is_absolute_uri(request.redirect_uri): - raise errors.InvalidRedirectURIError(request=request) + self._handle_redirects(request) # Then check for normal errors. From 06912287b170aa7255b7120403943ba9e99e649e Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 17 Dec 2018 15:12:22 +0200 Subject: [PATCH 145/237] Raise error on missing token. --- oauthlib/oauth2/rfc6749/endpoints/base.py | 8 +++++++- oauthlib/oauth2/rfc6749/endpoints/introspect.py | 6 ++---- oauthlib/oauth2/rfc6749/endpoints/revocation.py | 6 ++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/base.py b/oauthlib/oauth2/rfc6749/endpoints/base.py index cdb015f3..7a121fa5 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/base.py +++ b/oauthlib/oauth2/rfc6749/endpoints/base.py @@ -12,7 +12,7 @@ import logging from ..errors import (FatalClientError, OAuth2Error, ServerError, - TemporarilyUnavailableError) + TemporarilyUnavailableError, InvalidRequestError) log = logging.getLogger(__name__) @@ -39,6 +39,12 @@ def catch_errors(self): def catch_errors(self, catch_errors): self._catch_errors = catch_errors + def _raise_on_missing_token(self, request): + """Raise error on missing token.""" + if not request.token: + raise InvalidRequestError(request=request, + description='Missing token parameter.') + 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 7613acce..58b9a88e 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/introspect.py +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -117,10 +117,8 @@ def validate_introspect_request(self, request): .. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5 .. _`RFC6749`: http://tools.ietf.org/html/rfc6749 """ - if not request.token: - raise InvalidRequestError(request=request, - description='Missing token parameter.') - + self._raise_on_missing_token(request) + if self.request_validator.client_authentication_required(request): if not self.request_validator.authenticate_client(request): log.debug('Client authentication failed, %r.', request) diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py index d5b5b782..6f0081bc 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py +++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py @@ -116,9 +116,7 @@ def validate_revocation_request(self, request): .. _`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 """ - if not request.token: - raise InvalidRequestError(request=request, - description='Missing token parameter.') + self._raise_on_missing_token(request) if self.request_validator.client_authentication_required(request): if not self.request_validator.authenticate_client(request): @@ -126,7 +124,7 @@ def validate_revocation_request(self, request): raise InvalidClientError(request=request) elif not self.request_validator.authenticate_client_id(request.client_id, request): log.debug('Client authentication failed, %r.', request) - raise InvalidClientError(request=request) + raise InvalidClientError(request=request) if (request.token_type_hint and request.token_type_hint in self.valid_token_types and From cfd6af0168c27e74eb8fd300d42b3145cdea8a78 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 17 Dec 2018 15:16:44 +0200 Subject: [PATCH 146/237] Extract raising error on client auth failure. --- oauthlib/oauth2/rfc6749/endpoints/base.py | 12 +++++++++++- oauthlib/oauth2/rfc6749/endpoints/introspect.py | 12 ++---------- oauthlib/oauth2/rfc6749/endpoints/revocation.py | 12 ++---------- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/base.py b/oauthlib/oauth2/rfc6749/endpoints/base.py index 7a121fa5..638311d1 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/base.py +++ b/oauthlib/oauth2/rfc6749/endpoints/base.py @@ -12,7 +12,8 @@ import logging from ..errors import (FatalClientError, OAuth2Error, ServerError, - TemporarilyUnavailableError, InvalidRequestError) + TemporarilyUnavailableError, InvalidRequestError, + InvalidClientError) log = logging.getLogger(__name__) @@ -44,6 +45,15 @@ def _raise_on_missing_token(self, request): if not request.token: raise InvalidRequestError(request=request, description='Missing token parameter.') + def _raise_on_invalid_client(self, request): + """Raise on failed client authentication.""" + if self.request_validator.client_authentication_required(request): + if not self.request_validator.authenticate_client(request): + log.debug('Client authentication failed, %r.', request) + raise InvalidClientError(request=request) + elif not self.request_validator.authenticate_client_id(request.client_id, request): + log.debug('Client authentication failed, %r.', request) + raise InvalidClientError(request=request) def catch_errors_and_unavailability(f): diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py index 58b9a88e..5f24ff3d 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/introspect.py +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -14,8 +14,7 @@ from oauthlib.common import Request -from ..errors import (InvalidClientError, InvalidRequestError, OAuth2Error, - UnsupportedTokenTypeError) +from ..errors import OAuth2Error, UnsupportedTokenTypeError from .base import BaseEndpoint, catch_errors_and_unavailability log = logging.getLogger(__name__) @@ -118,14 +117,7 @@ def validate_introspect_request(self, request): .. _`RFC6749`: http://tools.ietf.org/html/rfc6749 """ self._raise_on_missing_token(request) - - if self.request_validator.client_authentication_required(request): - if not self.request_validator.authenticate_client(request): - log.debug('Client authentication failed, %r.', request) - raise InvalidClientError(request=request) - elif not self.request_validator.authenticate_client_id(request.client_id, request): - log.debug('Client authentication failed, %r.', request) - raise InvalidClientError(request=request) + self._raise_on_invalid_client(request) if (request.token_type_hint and request.token_type_hint in self.valid_token_types and diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py index 6f0081bc..8ec95127 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py +++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py @@ -13,8 +13,7 @@ from oauthlib.common import Request -from ..errors import (InvalidClientError, InvalidRequestError, OAuth2Error, - UnsupportedTokenTypeError) +from ..errors import OAuth2Error, UnsupportedTokenTypeError from .base import BaseEndpoint, catch_errors_and_unavailability log = logging.getLogger(__name__) @@ -117,14 +116,7 @@ def validate_revocation_request(self, request): .. _`RFC6749`: https://tools.ietf.org/html/rfc6749 """ self._raise_on_missing_token(request) - - if self.request_validator.client_authentication_required(request): - if not self.request_validator.authenticate_client(request): - log.debug('Client authentication failed, %r.', request) - raise InvalidClientError(request=request) - elif not self.request_validator.authenticate_client_id(request.client_id, request): - log.debug('Client authentication failed, %r.', request) - raise InvalidClientError(request=request) + self._raise_on_invalid_client(request) if (request.token_type_hint and request.token_type_hint in self.valid_token_types and From 79c667eedae4a4d447e8229e37eb844e3af05374 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 17 Dec 2018 15:19:51 +0200 Subject: [PATCH 147/237] Extract raising on unsupported token. --- oauthlib/oauth2/rfc6749/endpoints/base.py | 9 ++++++++- oauthlib/oauth2/rfc6749/endpoints/introspect.py | 6 +----- oauthlib/oauth2/rfc6749/endpoints/revocation.py | 6 +----- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/base.py b/oauthlib/oauth2/rfc6749/endpoints/base.py index 638311d1..3fd45c3f 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/base.py +++ b/oauthlib/oauth2/rfc6749/endpoints/base.py @@ -13,7 +13,7 @@ from ..errors import (FatalClientError, OAuth2Error, ServerError, TemporarilyUnavailableError, InvalidRequestError, - InvalidClientError) + InvalidClientError, UnsupportedTokenTypeError) log = logging.getLogger(__name__) @@ -55,6 +55,13 @@ def _raise_on_invalid_client(self, request): log.debug('Client authentication failed, %r.', request) raise InvalidClientError(request=request) + def _raise_on_unspported_token(self, request): + """Raise on unsupported tokens.""" + if (request.token_type_hint and + request.token_type_hint in self.valid_token_types and + request.token_type_hint not in self.supported_token_types): + raise UnsupportedTokenTypeError(request=request) + 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 5f24ff3d..25dae1f5 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/introspect.py +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -118,8 +118,4 @@ def validate_introspect_request(self, request): """ self._raise_on_missing_token(request) self._raise_on_invalid_client(request) - - if (request.token_type_hint and - request.token_type_hint in self.valid_token_types and - request.token_type_hint not in self.supported_token_types): - raise UnsupportedTokenTypeError(request=request) + self._raise_on_unspported_token(request) diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py index 8ec95127..f9a56489 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py +++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py @@ -117,8 +117,4 @@ def validate_revocation_request(self, request): """ self._raise_on_missing_token(request) self._raise_on_invalid_client(request) - - if (request.token_type_hint and - request.token_type_hint in self.valid_token_types and - request.token_type_hint not in self.supported_token_types): - raise UnsupportedTokenTypeError(request=request) + self._raise_on_unspported_token(request) From e9c6f01bc6f89e6b90f2c9b61e6a9878d5612147 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 17 Dec 2018 16:04:26 +0200 Subject: [PATCH 148/237] Added bandit to CI to avoid security issues (#627) * Added bandit to CI to avoid security issues. * Remove basepython for bandit. * Remove metrics. --- .travis.yml | 2 ++ bandit.json | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ tox.ini | 8 +++++++- 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 bandit.json diff --git a/.travis.yml b/.travis.yml index e304ce62..c7978d7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,8 @@ matrix: env: TOXENV=py36 - python: 3.7 env: TOXENV=py37 + - python: 3.7 + env: TOXENV=bandit - python: pypy3.5 env: TOXENV=pypy3 install: diff --git a/bandit.json b/bandit.json new file mode 100644 index 00000000..02e15a83 --- /dev/null +++ b/bandit.json @@ -0,0 +1,48 @@ +{ + "errors": [], + "generated_at": "2018-12-13T10:39:37Z", + "results": [ + { + "code": "182 if request.body is not None and content_type_eligible:\n183 params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8')))\n184 \n", + "filename": "oauthlib/oauth1/rfc5849/__init__.py", + "issue_confidence": "HIGH", + "issue_severity": "MEDIUM", + "issue_text": "Use of insecure MD2, MD4, MD5, or SHA1 hash function.", + "line_number": 183, + "line_range": [ + 183 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html#b303-md5", + "test_id": "B303", + "test_name": "blacklist" + }, + { + "code": "45 def __init__(self, endpoints, claims={}, raise_errors=True):\n46 assert isinstance(claims, dict)\n47 for endpoint in endpoints:\n", + "filename": "oauthlib/oauth2/rfc6749/endpoints/metadata.py", + "issue_confidence": "HIGH", + "issue_severity": "LOW", + "issue_text": "Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.", + "line_number": 46, + "line_range": [ + 46 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b101_assert_used.html", + "test_id": "B101", + "test_name": "assert_used" + }, + { + "code": "47 for endpoint in endpoints:\n48 assert isinstance(endpoint, BaseEndpoint)\n49 \n", + "filename": "oauthlib/oauth2/rfc6749/endpoints/metadata.py", + "issue_confidence": "HIGH", + "issue_severity": "LOW", + "issue_text": "Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.", + "line_number": 48, + "line_range": [ + 48 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b101_assert_used.html", + "test_id": "B101", + "test_name": "assert_used" + } + ] +} diff --git a/tox.ini b/tox.ini index 47237d8c..4893175b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36,py37,pypy,pypy3,docs,readme +envlist = py27,py34,py35,py36,py37,pypy,pypy3,docs,readme,bandit [testenv] deps= @@ -27,3 +27,9 @@ whitelist_externals=echo commands= python setup.py check -r -s echo setup.py/long description is syntaxly correct + +[testenv:bandit] +skipsdist=True +deps=bandit +commands=bandit -b bandit.json -r oauthlib/ +whitelist_externals=bandit From c5de837837d3b69edb6bb8bcfe1741047b992760 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 17 Dec 2018 16:59:52 +0200 Subject: [PATCH 149/237] Fix typo. --- oauthlib/oauth2/rfc6749/endpoints/base.py | 2 +- oauthlib/oauth2/rfc6749/endpoints/introspect.py | 2 +- oauthlib/oauth2/rfc6749/endpoints/revocation.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/base.py b/oauthlib/oauth2/rfc6749/endpoints/base.py index 3fd45c3f..c0fc7269 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/base.py +++ b/oauthlib/oauth2/rfc6749/endpoints/base.py @@ -55,7 +55,7 @@ def _raise_on_invalid_client(self, request): log.debug('Client authentication failed, %r.', request) raise InvalidClientError(request=request) - def _raise_on_unspported_token(self, request): + def _raise_on_unsupported_token(self, request): """Raise on unsupported tokens.""" if (request.token_type_hint and request.token_type_hint in self.valid_token_types and diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py index b10a8458..ff7a32d7 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/introspect.py +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -119,4 +119,4 @@ def validate_introspect_request(self, request): """ self._raise_on_missing_token(request) self._raise_on_invalid_client(request) - self._raise_on_unspported_token(request) + self._raise_on_unsupported_token(request) diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py index 00da64ab..4cd96a7e 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py +++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py @@ -123,4 +123,4 @@ def validate_revocation_request(self, request): """ self._raise_on_missing_token(request) self._raise_on_invalid_client(request) - self._raise_on_unspported_token(request) + self._raise_on_unsupported_token(request) From cecb278f4467f54a0fe358b7a5f1ba43dae16286 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 26 Dec 2018 14:37:39 -0500 Subject: [PATCH 150/237] Remove unused 'requires' variable form setup.py --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index d2a27a0c..e27bb1c3 100755 --- a/setup.py +++ b/setup.py @@ -25,8 +25,6 @@ def fread(fn): signedtoken_require = ['cryptography', 'pyjwt>=1.0.0'] signals_require = ['blinker'] -requires = [] - setup( name='oauthlib', version=oauthlib.__version__, @@ -47,7 +45,6 @@ def fread(fn): 'signedtoken': signedtoken_require, 'signals': signals_require, }, - install_requires=requires, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', From d8fe024751fad2b0fdb989f175f2b64d81f8880a Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 26 Dec 2018 15:56:39 -0500 Subject: [PATCH 151/237] Remove unused 'tests_require' from setup.py (#638) Neither used by Travis CI nor by tox.ini. The mock package was out of sync with requirements-tests.txt for Python 3 environments. Rather than maintain this duplicate, unused list of requirements just remove it. --- setup.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.py b/setup.py index e27bb1c3..c0bdf38d 100755 --- a/setup.py +++ b/setup.py @@ -18,9 +18,7 @@ def fread(fn): with open(join(dirname(__file__), fn), 'r') as f: return f.read() -tests_require = ['cryptography', 'pyjwt>=1.0.0', 'blinker'] -if sys.version_info[0] == 2: - tests_require.append('mock>=2.0') + rsa_require = ['cryptography'] signedtoken_require = ['cryptography', 'pyjwt>=1.0.0'] signals_require = ['blinker'] @@ -38,9 +36,7 @@ def fread(fn): platforms='any', license='BSD', packages=find_packages(exclude=('docs', 'tests', 'tests.*')), - tests_require=tests_require, extras_require={ - 'test': tests_require, 'rsa': rsa_require, 'signedtoken': signedtoken_require, 'signals': signals_require, From 729fb9fa17efb8a38bd50f5e180802d6034c351b Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Thu, 27 Dec 2018 04:16:59 -0500 Subject: [PATCH 152/237] Pass python_requires argument to setuptools (#636) Helps pip decide what version of the library to install. https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires > If your project only runs on certain Python versions, setting the > python_requires argument to the appropriate PEP 440 version specifier > string will prevent pip from installing the project on other Python > versions. https://setuptools.readthedocs.io/en/latest/setuptools.html#new-and-changed-setup-keywords > python_requires > > A string corresponding to a version specifier (as defined in PEP 440) > for the Python version, used to specify the Requires-Python defined in > PEP 345. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index c0bdf38d..3f822e0b 100755 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ def fread(fn): platforms='any', license='BSD', packages=find_packages(exclude=('docs', 'tests', 'tests.*')), + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', extras_require={ 'rsa': rsa_require, 'signedtoken': signedtoken_require, From b79b5511b01c230037abee5b636f6f03378c93f1 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Thu, 27 Dec 2018 04:44:22 -0500 Subject: [PATCH 153/237] Fix invalid escape sequence in tests (#637) Fixes warning when running tests: ``` tests/oauth1/rfc5849/endpoints/test_base.py:63 oauthlib/tests/oauth1/rfc5849/endpoints/test_base.py:63: DeprecationWarning: invalid escape sequence \d headers['Authorization'] = sub('timestamp="\d*k?"', ``` --- tests/oauth1/rfc5849/endpoints/test_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/oauth1/rfc5849/endpoints/test_base.py b/tests/oauth1/rfc5849/endpoints/test_base.py index 8c41cf24..60f7860e 100644 --- a/tests/oauth1/rfc5849/endpoints/test_base.py +++ b/tests/oauth1/rfc5849/endpoints/test_base.py @@ -60,7 +60,7 @@ def test_invalid_version(self): def test_expired_timestamp(self): headers = {} for pattern in ('12345678901', '4567890123', '123456789K'): - headers['Authorization'] = sub('timestamp="\d*k?"', + headers['Authorization'] = sub(r'timestamp="\d*k?"', 'timestamp="%s"' % pattern, self.headers['Authorization']) h, b, s = self.endpoint.create_request_token_response( From d4f48845a7ceec5bbd658cf2b478f7b6d5cfee2e Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 28 Dec 2018 19:00:45 +0100 Subject: [PATCH 154/237] Add OAuth2 Provider oauthlib-flow --- docs/Makefile | 2 +- docs/conf.py | 9 +- docs/oauth2/oauth2provider-legend.dot | 32 ++++ docs/oauth2/oauth2provider-server.dot | 215 ++++++++++++++++++++++++++ docs/oauth2/server.rst | 22 ++- 5 files changed, 271 insertions(+), 9 deletions(-) create mode 100644 docs/oauth2/oauth2provider-legend.dot create mode 100644 docs/oauth2/oauth2provider-server.dot diff --git a/docs/Makefile b/docs/Makefile index 9ec7a6da..d134c96f 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,7 +2,7 @@ # # You can set these variables from the command line. -SPHINXOPTS = +SPHINXOPTS = -v SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build diff --git a/docs/conf.py b/docs/conf.py index 2594e387..fadb9136 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,11 +21,16 @@ # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +needs_sphinx = '1.1' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.viewcode'] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.viewcode', + 'sphinx.ext.graphviz' +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/oauth2/oauth2provider-legend.dot b/docs/oauth2/oauth2provider-legend.dot new file mode 100644 index 00000000..746ac2b2 --- /dev/null +++ b/docs/oauth2/oauth2provider-legend.dot @@ -0,0 +1,32 @@ +digraph oauthlib_legend { + + subgraph cluster_legend { + label="Legend"; + + /* + method [ shape=record; label="{{RequestValidator\nmethod name|arguments}|return values}" ]; + endpoint [ shape=record; label="{Endpoint name|{function name|arguments}|grant type}" ]; + webframework [ shape=hexagon; label="Upstream functions" ]; + */ + + flow_code_token [shape=none,label="Authorization Code\nAccess Token Request"]; + flow_code_auth [shape=none,label="Authorization Code\nAuthorization Request"]; + flow_implicit [shape=none,label="Implicit Grant"]; + flow_password [shape=none,label="Resource Owner Password\nCredentials Grant"]; + flow_clicreds [shape=none,label="Client Credentials Grant"]; + flow_refresh [shape=none,label="Refresh Grant"]; + flow_introspect [shape=none,label="Token Introspection"]; + flow_revoke [shape=none,label="Token Revoke"]; + flow_resource [shape=none,label="Resource Access"]; + flow_code_token -> a [style=bold,color=green]; + flow_code_auth -> b [style=bold,color=darkgreen]; + flow_implicit -> c [style=bold,color=orange]; + flow_password -> d [style=bold,color=red]; + flow_clicreds -> e [style=bold,color=blue]; + flow_refresh -> f [style=bold,color=brown]; + flow_introspect -> g [style=bold,color=yellow]; + flow_revoke -> h [style=bold,color=purple]; + flow_resource -> i [style=bold,color=pink]; + a, b, c, d, e, f, g, h, i [shape=none,label=""]; + } +} diff --git a/docs/oauth2/oauth2provider-server.dot b/docs/oauth2/oauth2provider-server.dot new file mode 100644 index 00000000..bf7df75f --- /dev/null +++ b/docs/oauth2/oauth2provider-server.dot @@ -0,0 +1,215 @@ +digraph oauthlib { + center="1" + edge [ style=bold ]; + + /* Web Framework Entry and Exit points */ + { + node [ shape=hexagon ]; + edge [ style=normal ]; + + webapi_request [ label="WebFramework\nHTTP request" ]; + webapi_request:s -> + endpoint_authorize:top:n, + endpoint_token:top:n, + endpoint_introspect:top:n, + endpoint_revoke:top:n, + endpoint_resource:top:n; + webapi_response [ label="WebFramework\nHTTP response" ]; + } + + /* OAuthlib Endpoints */ + { + rank=same; + + endpoint_authorize [ shape=record; label="{Authorize Endpoint|{create_authorize_response|{uri|method|body|headers|credentials}}|{token|code}}" ]; + endpoint_token [ shape=record; label="{Token Endpoint|{create_token_response|{uri|method|body|headers|credentials}}|{authorization_code|password|client_credentials|refresh_token}}" ]; + endpoint_revoke [ shape=record; label="{Revocation Endpoint|{create_revocation_response|{uri|method|body|headers}}}" ]; + endpoint_introspect [ shape=record; label="{Introspect Endpoint|{create_introspect_response|{uri|method|body|headers}}}" ]; + endpoint_resource [ shape=record; label="{Resource Endpoint|{verify_request|{uri|method|body|headers|scopes_list}}}" ]; + } + + /* OAuthlib RequestValidator Methods */ + { + node [ shape=record ]; + + f_client_authentication_required [ label="{{client_authentication_required|request}|{True|False}}"; ]; + f_authenticate_client [ label="{{authenticate_client|request}|{True|False}}";]; + f_authenticate_client_id [ label="{{authenticate_client_id|{client_id|request}}|{True|False}}"; ]; + f_validate_grant_type [ label="{{validate_grant_type|{client_id|grant_type|client|request}}|{True|False}}"; ]; + f_validate_code [ label="{{validate_code|{client_id|code|request}}|{True|False}}"; ]; + f_confirm_redirect_uri [ label="{{confirm_redirect_uri|{client_id|code|redirect_uri|client|request}}|{True|False}}"; ]; + f_get_default_redirect_uri [ label="{{get_default_redirect_uri|{client_id|request}}|{redirect_uri|None}}"; ]; + f_invalidate_authorization_code [ label="{{invalidate_authorization_code|{client_id|code|request}}|None}"; ]; + f_validate_scopes [ label="{{validate_scopes|{client_id|scopes|client|request}}|{True|False}}"; ]; + f_save_bearer_token [ label="{{save_bearer_token|{token|request}}|None}"; ]; + f_revoke_token [ label="{{revoke_token|{token|token_type_hint|request}}|None}"; ]; + f_validate_client_id [ label="{{validate_client_id|{client_id|request}}|{True|False}}"; ]; + f_validate_redirect_uri [ label="{{validate_redirect_uri|{client_id|redirect_uri|request}}|{True|False}}"; ]; + f_is_pkce_required [ label="{{is_pkce_required|{client_id|request}}|{True|False}}"; ]; + f_validate_response_type [ label="{{validate_response_type|{client_id|response_type|client|request}}|{True|False}}"; ]; + f_save_authorization_code [ label="{{save_authorization_code|{client_id|code|request}}|None}"; ]; + f_validate_bearer_token [ label="{{validate_bearer_token|{token|scopes|request}}|{True|False}}"; ]; + f_validate_refresh_token [ label="{{validate_refresh_token|{refresh_token|client|request}}|{True|False}}"; ]; + f_get_default_scopes [ label="{{get_default_scopes|{client_id|request}}|{[scopes]}}"; ]; + f_get_original_scopes [ label="{{get_original_scopes|{refresh_token|request}}|{[scopes]}}"; ]; + f_is_within_original_scope [ label="{{is_within_original_scope|{refresh_scopes|refresh_token|request}}|{True|False}}"; ]; + f_validate_user [ label="{{validate_user|{username|password|client|request}}|{True|False}}"; ]; + f_introspect_token [ label="{{introspect_token|{token|token_type_hint|request}}|{\{claims\}|None}}"; ]; + } + + /* OAuthlib Conditions */ + + if_code_challenge [ label="if code_challenge"; ]; + if_redirect_uri [ label="if redirect_uri"; ]; + if_redirect_uri_present [ shape=none;label="present"; ]; + if_redirect_uri_missing [ shape=none;label="missing"; ]; + if_scopes [ label="if scopes"; ]; + if_all [ label="all(request_scopes not in scopes)"; ]; + + /* OAuthlib errors */ + e_normal [ shape=none,label="ERROR" ]; + + /* Authorization Code - Access Token Request */ + { + edge [ color=green ]; + + endpoint_token:authorization_code:s -> f_client_authentication_required; + f_client_authentication_required:true:s -> f_authenticate_client; + f_client_authentication_required:false -> f_authenticate_client_id; + f_authenticate_client:true:s -> f_validate_grant_type; + f_authenticate_client_id:true:s -> f_validate_grant_type; + f_validate_grant_type:true:s -> f_validate_code; + + f_validate_code:true:s -> if_redirect_uri; + if_redirect_uri -> if_redirect_uri_present [ arrowhead=none ]; + if_redirect_uri -> if_redirect_uri_missing [ arrowhead=none ]; + if_redirect_uri_present -> f_confirm_redirect_uri; + if_redirect_uri_missing -> f_get_default_redirect_uri; + + f_confirm_redirect_uri:true:s -> f_save_bearer_token; + f_get_default_redirect_uri -> f_save_bearer_token; + + f_save_bearer_token -> f_invalidate_authorization_code; + f_invalidate_authorization_code -> webapi_response; + } + /* Authorization Code - Authorization Request */ + { + edge [ color=darkgreen ]; + + endpoint_authorize:code:s -> f_validate_client_id; + f_validate_client_id:true:s -> if_redirect_uri; + if_redirect_uri -> if_redirect_uri_present [ arrowhead=none ]; + if_redirect_uri -> if_redirect_uri_missing [ arrowhead=none ]; + if_redirect_uri_present -> f_validate_redirect_uri; + if_redirect_uri_missing -> f_get_default_redirect_uri; + + f_validate_redirect_uri:true:s -> f_validate_response_type; + f_get_default_redirect_uri -> f_validate_response_type; + f_validate_response_type:true:s -> f_is_pkce_required; + f_is_pkce_required:true:s -> if_code_challenge; + f_is_pkce_required:false -> f_validate_scopes; + + if_code_challenge -> f_validate_scopes [ label="present" ]; + if_code_challenge -> e_normal [ label="missing" ]; + + f_validate_scopes:true:s -> f_save_authorization_code; + } + + /* Implicit */ + { + edge [ color=orange ]; + + endpoint_authorize:token:s -> f_validate_client_id; + f_validate_client_id:true:s -> if_redirect_uri; + if_redirect_uri -> if_redirect_uri_present [ arrowhead=none ]; + if_redirect_uri -> if_redirect_uri_missing [ arrowhead=none ]; + if_redirect_uri_present -> f_validate_redirect_uri; + if_redirect_uri_missing -> f_get_default_redirect_uri; + + f_validate_redirect_uri:true:s -> f_validate_response_type; + f_get_default_redirect_uri -> f_validate_response_type; + f_validate_response_type:true:s -> f_validate_scopes; + f_validate_scopes:true:s -> f_save_bearer_token; + } + + /* Resource Owner Password Grant */ + { + edge [ color=red ]; + + endpoint_token:password:s -> f_client_authentication_required; + f_client_authentication_required:true:s -> f_authenticate_client; + f_client_authentication_required:false -> f_authenticate_client_id; + f_authenticate_client:true:s -> f_validate_user; + f_authenticate_client_id:true:s -> f_validate_user; + f_validate_user:true:s -> f_validate_grant_type; + + f_validate_grant_type:true:s -> if_scopes; + if_scopes -> f_validate_scopes [ label="present" ]; + if_scopes -> f_get_default_scopes [ label="missing" ]; + + f_validate_scopes:true:s -> f_save_bearer_token; + f_get_default_scopes -> f_save_bearer_token; + f_save_bearer_token -> webapi_response; + } + + /* Client Credentials Grant */ + { + edge [ color=blue ]; + + endpoint_token:client_credentials:s -> f_authenticate_client; + f_authenticate_client -> f_validate_grant_type; + f_validate_grant_type:true:s -> f_validate_scopes; + f_validate_scopes:true:s -> f_save_bearer_token; + f_save_bearer_token -> webapi_response; + } + + /* Refresh Grant */ + { + edge [ color=brown ]; + + endpoint_token:refresh_token:s -> f_client_authentication_required; + f_client_authentication_required:true:s -> f_authenticate_client; + f_client_authentication_required:false -> f_authenticate_client_id; + f_authenticate_client:true:s -> f_validate_grant_type; + f_authenticate_client_id:true:s -> f_validate_grant_type; + f_validate_grant_type:true:s -> f_validate_refresh_token; + f_validate_refresh_token:true:s -> f_get_original_scopes; + f_get_original_scopes -> if_all; + if_all -> f_is_within_original_scope [ label="True" ]; + if_all -> f_save_bearer_token [ label="False" ]; + f_is_within_original_scope:true:s -> f_save_bearer_token; + f_save_bearer_token -> webapi_response; + } + + /* Introspect Endpoint */ + { + edge [ color=yellow ]; + + endpoint_introspect:s -> f_client_authentication_required [ label="" ]; + f_client_authentication_required:true:s -> f_authenticate_client; + f_client_authentication_required:false -> f_authenticate_client_id; + f_authenticate_client:true:s -> f_introspect_token; + f_authenticate_client_id:true:s -> f_introspect_token; + f_introspect_token:claims -> webapi_response; + } + + /* Revocation Endpoint */ + { + edge [ color=purple ]; + + endpoint_revoke:s -> f_client_authentication_required; + f_client_authentication_required:true:s -> f_authenticate_client; + f_client_authentication_required:false -> f_authenticate_client_id; + f_authenticate_client:true:s -> f_revoke_token; + f_authenticate_client_id:true:s -> f_revoke_token; + f_revoke_token:s -> webapi_response; + } + + /* Resource Access - Verify Request */ + { + edge [ color=pink ]; + + endpoint_resource:s -> f_validate_bearer_token; + f_validate_bearer_token:true -> webapi_response; + } +} diff --git a/docs/oauth2/server.rst b/docs/oauth2/server.rst index 6c065c57..dad0aae1 100644 --- a/docs/oauth2/server.rst +++ b/docs/oauth2/server.rst @@ -25,7 +25,17 @@ as well as provide an interface for a backend to store tokens, clients, etc. .. contents:: Tutorial Contents :depth: 3 -1. Create your datastore models +1. OAuth2.0 Provider flows +------------------------------- + +OAuthLib interface between web framework and provider implementation are not always easy to follow, it's why a graph below has been done to better understand the implication of OAuthLib in the request's lifecycle. + + +.. graphviz:: oauth2provider-legend.dot +.. graphviz:: oauth2provider-server.dot + + +2. Create your datastore models ------------------------------- These models will represent various OAuth specific concepts. There are a few @@ -257,7 +267,7 @@ the token. challenge_method = django.db.models.CharField(max_length=6) -2. Implement a validator +3. Implement a validator ------------------------ The majority of the work involved in implementing an OAuth 2 provider @@ -301,7 +311,7 @@ Relevant sections include: security -3. Create your composite endpoint +4. Create your composite endpoint --------------------------------- Each of the endpoints can function independently from each other, however @@ -326,7 +336,7 @@ Relevant sections include: preconfigured_servers -4. Create your endpoint views +5. Create your endpoint views ----------------------------- We are implementing support for the Authorization Code Grant and will @@ -430,7 +440,7 @@ The example using Django but should be transferable to any framework. return HttpResponseBadRequest('Evil client is unable to send a proper request. Error is: ' + e.description) -5. Protect your APIs using scopes +6. Protect your APIs using scopes --------------------------------- Let's define a decorator we can use to protect the views. @@ -501,7 +511,7 @@ at runtime by a function, rather then by a list. # A view that has its views functionally set. return HttpResponse('pictures of cats') -6. Let us know how it went! +7. Let us know how it went! --------------------------- Drop a line in our `Gitter OAuthLib community`_ or open a `GitHub issue`_ =) From 45135a25d4dde6f0d1d6a9b735a40159ac391c11 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 28 Dec 2018 19:23:57 +0100 Subject: [PATCH 155/237] Update Changelog to 3.0.0 --- CHANGELOG.rst | 42 ++++++++++++++++++++++++++++++++++++++++-- oauthlib/__init__.py | 2 +- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fd537691..3dea1038 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,9 +1,33 @@ Changelog ========= -Unreleased +3.0.0 (2019-01-01) ------------------ - +OAuth2.0 Provider - outstanding Features + +* OpenID Connect Core support +* RFC7662 Introspect support +* RFC8414 OAuth2.0 Authorization Server Metadata support (#605) +* RFC7636 PKCE support (#617 #624) + +OAuth2.0 Provider - Bugfixes + +* Add "request" to confirm_redirect_uri #504 +* confirm_redirect_uri/get_default_redirect_uri has a bit changed #445 +* empty scopes no longer raise exceptions for implicit and authorization_code #475 / #406 +* invalid_client is now a FatalError #606 +* Changed errors status code from 401 to 400: +- invalid_grant: #264 +- invalid_scope: #620 +- access_denied/unauthorized_client/consent_required/login_required #623 +- 401 must have WWW-Authenticate HTTP Header set. #623 + +OAuth2.0 Client - Bugfixes / Changes: + +* expires_in in Implicit flow is now an integer #569 +* expires is no longer overriding expires_in #506 +* parse_request_uri_response is now required #499 +* Unknown error=xxx raised by OAuth2 providers was not understood #431 * OAuth2's `prepare_token_request` supports sending an empty string for `client_id` (#585) * OAuth2's `WebApplicationClient.prepare_request_body` was refactored to better support sending or omitting the `client_id` via a new `include_client_id` kwarg. @@ -11,6 +35,20 @@ Unreleased a `client_id` parameter is submitted; the already configured `self.client_id` is the preferred option. (#585) +OAuth1.0 Client: + +* Support for HMAC-SHA256 #498 + +General fixes: + +* $ and ' are allowed to be unencoded in query strings #564 +* Request attributes are no longer overriden by HTTP Headers #409 +* Removed unnecessary code for handling python2.6 +* Add support of python3.7 #621 +* Several minors updates to setup.py and tox +* Set pytest as the default unittest framework + + 2.1.0 (2018-05-21) ------------------ diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 5b1b3809..e4ee2fd9 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,6 +12,6 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.0.0-dev' +__version__ = '3.0.0' logging.getLogger('oauthlib').addHandler(NullHandler()) From f4273e75250dd36c88d63dc075ae45650a5172e9 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 28 Dec 2018 19:24:11 +0100 Subject: [PATCH 156/237] Bump to 2019 --- LICENSE | 2 +- docs/conf.py | 2 +- oauthlib/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 84b5c756..d5a9e9ac 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018 The OAuthlib Community +Copyright (c) 2019 The OAuthlib Community All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/docs/conf.py b/docs/conf.py index fadb9136..bd8750ee 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,7 +46,7 @@ # General information about the project. project = u'OAuthLib' -copyright = u'2018, The OAuthlib Community' +copyright = u'2019, The OAuthlib Community' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index e4ee2fd9..b37d288c 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -5,7 +5,7 @@ A generic, spec-compliant, thorough implementation of the OAuth request-signing logic. - :copyright: (c) 2018 by The OAuthlib Community + :copyright: (c) 2019 by The OAuthlib Community :license: BSD, see LICENSE for details. """ import logging From 213a47cf5fc9672271d98b898683727dafe0570b Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 28 Dec 2018 19:24:30 +0100 Subject: [PATCH 157/237] Replace latest occurences of Gazit w/ new community --- docs/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index bd8750ee..f1a489ad 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -191,7 +191,7 @@ # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'OAuthLib.tex', u'OAuthLib Documentation', - u'Idan Gazit and the Python Community', 'manual'), + u'The OAuhthlib Community', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -221,7 +221,7 @@ # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'oauthlib', u'OAuthLib Documentation', - [u'Idan Gazit and the Python Community'], 1) + [u'The OAuthlib Community'], 1) ] # If true, show URL addresses after external links. @@ -235,7 +235,7 @@ # dir menu entry, description, category) texinfo_documents = [ ('index', 'OAuthLib', u'OAuthLib Documentation', - u'Idan Gazit and the Python Community', 'OAuthLib', 'One line description of project.', + u'The OAuthlib Community', 'OAuthLib', 'One line description of project.', 'Miscellaneous'), ] From fa0b1549546d8c7dc1045ea637a8f8afd0d39a83 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 7 Jan 2019 10:22:22 +0100 Subject: [PATCH 158/237] Add Breaking Changes section & split Bugfixes --- CHANGELOG.rst | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3dea1038..2cc0dd37 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,17 +10,20 @@ OAuth2.0 Provider - outstanding Features * RFC8414 OAuth2.0 Authorization Server Metadata support (#605) * RFC7636 PKCE support (#617 #624) -OAuth2.0 Provider - Bugfixes +OAuth2.0 Provider - API/Breaking Changes * Add "request" to confirm_redirect_uri #504 * confirm_redirect_uri/get_default_redirect_uri has a bit changed #445 -* empty scopes no longer raise exceptions for implicit and authorization_code #475 / #406 * invalid_client is now a FatalError #606 * Changed errors status code from 401 to 400: -- invalid_grant: #264 -- invalid_scope: #620 -- access_denied/unauthorized_client/consent_required/login_required #623 -- 401 must have WWW-Authenticate HTTP Header set. #623 + - invalid_grant: #264 + - invalid_scope: #620 + - access_denied/unauthorized_client/consent_required/login_required #623 + - 401 must have WWW-Authenticate HTTP Header set. #623 + +OAuth2.0 Provider - Bugfixes + +* empty scopes no longer raise exceptions for implicit and authorization_code #475 / #406 OAuth2.0 Client - Bugfixes / Changes: From 20d116c0db616285ca48ef1591a8a79796a76f5d Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 8 Jan 2019 15:43:27 +0100 Subject: [PATCH 159/237] Fixed graphviz/dot graph & improved clarity (#642) I fixed graphviz missing output to web responses (see image of https://github.com/oauthlib/oauthlib/pull/639), and I have added a fixed rank (`rank=same`) when functions are achieving an identical goal. E.g. `validate_client_id`, `validate_user`, `validate_bearer_token` are unique for each flows, or, e.g. `confirm_redirect_uri`, `validate_redirect_uri` together, and so on. ![graphviz-0cc58e8637b94d7402eda45a1fef6e68889bd8e1](https://user-images.githubusercontent.com/820496/50830407-042ad600-1348-11e9-936a-03d07f42494f.png) --- docs/oauth2/oauth2provider-legend.dot | 4 +- docs/oauth2/oauth2provider-server.dot | 103 +++++++++++++++++++------- 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/docs/oauth2/oauth2provider-legend.dot b/docs/oauth2/oauth2provider-legend.dot index 746ac2b2..ad87d80b 100644 --- a/docs/oauth2/oauth2provider-legend.dot +++ b/docs/oauth2/oauth2provider-legend.dot @@ -18,8 +18,8 @@ digraph oauthlib_legend { flow_introspect [shape=none,label="Token Introspection"]; flow_revoke [shape=none,label="Token Revoke"]; flow_resource [shape=none,label="Resource Access"]; - flow_code_token -> a [style=bold,color=green]; - flow_code_auth -> b [style=bold,color=darkgreen]; + flow_code_token -> a [style=bold,color=darkgreen]; + flow_code_auth -> b [style=bold,color=green]; flow_implicit -> c [style=bold,color=orange]; flow_password -> d [style=bold,color=red]; flow_clicreds -> e [style=bold,color=blue]; diff --git a/docs/oauth2/oauth2provider-server.dot b/docs/oauth2/oauth2provider-server.dot index bf7df75f..ec240787 100644 --- a/docs/oauth2/oauth2provider-server.dot +++ b/docs/oauth2/oauth2provider-server.dot @@ -1,4 +1,11 @@ digraph oauthlib { + /* Naming conventions: + f_ : functions in shape=record + endpoint_ : endpoints in shape=record + webapi_ : oauthlib entry/exit points in shape=hexagon + if_ : internal conditions + r_ : used when returning from two functions into one for improving clarity + */ center="1" edge [ style=bold ]; @@ -66,18 +73,58 @@ digraph oauthlib { if_scopes [ label="if scopes"; ]; if_all [ label="all(request_scopes not in scopes)"; ]; + /* OAuthlib functions returns helpers */ + r_client_authenticated [ shape=none,label="client authenticated"; ]; + /* OAuthlib errors */ e_normal [ shape=none,label="ERROR" ]; + /* Ranking by functional roles */ + { + rank = same; + f_validate_client_id; + f_validate_code; + /* f_validate_user; */ + f_validate_bearer_token; + f_validate_refresh_token; + f_introspect_token; + f_revoke_token; + } + { + rank = same; + f_validate_redirect_uri; + f_confirm_redirect_uri; + } + { + rank = same; + f_save_bearer_token; + f_save_authorization_code; + } + { + rank = same; + f_invalidate_authorization_code; + } + { + rank = same; + f_validate_scopes; + f_get_original_scopes; + f_get_default_scopes; + } + { + rank = same; + f_is_within_original_scope; + } + /* Authorization Code - Access Token Request */ { - edge [ color=green ]; + edge [ color=darkgreen ]; endpoint_token:authorization_code:s -> f_client_authentication_required; f_client_authentication_required:true:s -> f_authenticate_client; - f_client_authentication_required:false -> f_authenticate_client_id; - f_authenticate_client:true:s -> f_validate_grant_type; - f_authenticate_client_id:true:s -> f_validate_grant_type; + f_client_authentication_required:false:s -> f_authenticate_client_id; + f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ]; + f_authenticate_client_id:true:s -> r_client_authenticated [ arrowhead=none ]; + r_client_authenticated -> f_validate_grant_type; f_validate_grant_type:true:s -> f_validate_code; f_validate_code:true:s -> if_redirect_uri; @@ -85,16 +132,15 @@ digraph oauthlib { if_redirect_uri -> if_redirect_uri_missing [ arrowhead=none ]; if_redirect_uri_present -> f_confirm_redirect_uri; if_redirect_uri_missing -> f_get_default_redirect_uri; + f_get_default_redirect_uri:redirect_uri:s -> f_confirm_redirect_uri; f_confirm_redirect_uri:true:s -> f_save_bearer_token; - f_get_default_redirect_uri -> f_save_bearer_token; - f_save_bearer_token -> f_invalidate_authorization_code; f_invalidate_authorization_code -> webapi_response; } /* Authorization Code - Authorization Request */ { - edge [ color=darkgreen ]; + edge [ color=green ]; endpoint_authorize:code:s -> f_validate_client_id; f_validate_client_id:true:s -> if_redirect_uri; @@ -104,15 +150,16 @@ digraph oauthlib { if_redirect_uri_missing -> f_get_default_redirect_uri; f_validate_redirect_uri:true:s -> f_validate_response_type; - f_get_default_redirect_uri -> f_validate_response_type; + f_get_default_redirect_uri:redirect_uri:s -> f_validate_response_type; f_validate_response_type:true:s -> f_is_pkce_required; f_is_pkce_required:true:s -> if_code_challenge; - f_is_pkce_required:false -> f_validate_scopes; + f_is_pkce_required:false:s -> f_validate_scopes; if_code_challenge -> f_validate_scopes [ label="present" ]; - if_code_challenge -> e_normal [ label="missing" ]; + if_code_challenge -> e_normal [ label="missing",style=dashed ]; f_validate_scopes:true:s -> f_save_authorization_code; + f_save_authorization_code -> webapi_response; } /* Implicit */ @@ -127,9 +174,10 @@ digraph oauthlib { if_redirect_uri_missing -> f_get_default_redirect_uri; f_validate_redirect_uri:true:s -> f_validate_response_type; - f_get_default_redirect_uri -> f_validate_response_type; + f_get_default_redirect_uri:redirect_uri:s -> f_validate_response_type; f_validate_response_type:true:s -> f_validate_scopes; f_validate_scopes:true:s -> f_save_bearer_token; + f_save_bearer_token -> webapi_response; } /* Resource Owner Password Grant */ @@ -138,9 +186,10 @@ digraph oauthlib { endpoint_token:password:s -> f_client_authentication_required; f_client_authentication_required:true:s -> f_authenticate_client; - f_client_authentication_required:false -> f_authenticate_client_id; - f_authenticate_client:true:s -> f_validate_user; - f_authenticate_client_id:true:s -> f_validate_user; + f_client_authentication_required:false:s -> f_authenticate_client_id; + f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ]; + f_authenticate_client_id:true:s -> r_client_authenticated [ arrowhead=none ]; + r_client_authenticated -> f_validate_user; f_validate_user:true:s -> f_validate_grant_type; f_validate_grant_type:true:s -> if_scopes; @@ -157,7 +206,7 @@ digraph oauthlib { edge [ color=blue ]; endpoint_token:client_credentials:s -> f_authenticate_client; - f_authenticate_client -> f_validate_grant_type; + f_authenticate_client:true:s -> f_validate_grant_type; f_validate_grant_type:true:s -> f_validate_scopes; f_validate_scopes:true:s -> f_save_bearer_token; f_save_bearer_token -> webapi_response; @@ -169,9 +218,11 @@ digraph oauthlib { endpoint_token:refresh_token:s -> f_client_authentication_required; f_client_authentication_required:true:s -> f_authenticate_client; - f_client_authentication_required:false -> f_authenticate_client_id; - f_authenticate_client:true:s -> f_validate_grant_type; - f_authenticate_client_id:true:s -> f_validate_grant_type; + f_client_authentication_required:false:s -> f_authenticate_client_id; + f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ]; + f_authenticate_client_id:true:s -> r_client_authenticated [ arrowhead=none ]; + r_client_authenticated -> f_validate_grant_type; + f_validate_grant_type:true:s -> f_validate_refresh_token; f_validate_refresh_token:true:s -> f_get_original_scopes; f_get_original_scopes -> if_all; @@ -185,11 +236,12 @@ digraph oauthlib { { edge [ color=yellow ]; - endpoint_introspect:s -> f_client_authentication_required [ label="" ]; + endpoint_introspect:s -> f_client_authentication_required; f_client_authentication_required:true:s -> f_authenticate_client; - f_client_authentication_required:false -> f_authenticate_client_id; - f_authenticate_client:true:s -> f_introspect_token; - f_authenticate_client_id:true:s -> f_introspect_token; + f_client_authentication_required:false:s -> f_authenticate_client_id; + f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ]; + f_authenticate_client_id:true:s -> r_client_authenticated [ arrowhead=none ]; + r_client_authenticated -> f_introspect_token; f_introspect_token:claims -> webapi_response; } @@ -199,9 +251,10 @@ digraph oauthlib { endpoint_revoke:s -> f_client_authentication_required; f_client_authentication_required:true:s -> f_authenticate_client; - f_client_authentication_required:false -> f_authenticate_client_id; - f_authenticate_client:true:s -> f_revoke_token; - f_authenticate_client_id:true:s -> f_revoke_token; + f_client_authentication_required:false:s -> f_authenticate_client_id; + f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ]; + f_authenticate_client_id:true:s -> r_client_authenticated [ arrowhead=none ]; + r_client_authenticated -> f_revoke_token; f_revoke_token:s -> webapi_response; } From 7586b0b1f39b19d0779d9d7caa967a3f66c09702 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 11 Jan 2019 10:02:55 +0100 Subject: [PATCH 160/237] Fix 644, Add tests for BasicAuth credentials for all endpoints (#645) Test Introspect, Revoke, Token (web, legacy, backend) endpoints with authenticate_client and HTTP Basic Auth. --- .../oauth2/rfc6749/endpoints/introspect.py | 10 ++-- .../oauth2/rfc6749/endpoints/revocation.py | 6 +- .../endpoints/test_client_authentication.py | 58 ++++++++++++++++++- .../endpoints/test_introspect_endpoint.py | 1 - 4 files changed, 65 insertions(+), 10 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py index ff7a32d7..47022fd0 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/introspect.py +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -56,7 +56,7 @@ def create_introspect_response(self, uri, http_method='POST', body=None, an introspection response indicating the token is not active as described in Section 2.2. """ - headers = { + resp_headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'Pragma': 'no-cache', @@ -67,8 +67,8 @@ def create_introspect_response(self, uri, http_method='POST', body=None, log.debug('Token introspect valid for %r.', request) except OAuth2Error as e: log.debug('Client error during validation of %r. %r.', request, e) - headers.update(e.headers) - return headers, e.json, e.status_code + resp_headers.update(e.headers) + return resp_headers, e.json, e.status_code claims = self.request_validator.introspect_token( request.token, @@ -76,10 +76,10 @@ def create_introspect_response(self, uri, http_method='POST', body=None, request ) if claims is None: - return headers, json.dumps(dict(active=False)), 200 + return resp_headers, json.dumps(dict(active=False)), 200 if "active" in claims: claims.pop("active") - return headers, json.dumps(dict(active=True, **claims)), 200 + return resp_headers, json.dumps(dict(active=True, **claims)), 200 def validate_introspect_request(self, request): """Ensure the request is valid. diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py index 4cd96a7e..fda3f30e 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py +++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py @@ -58,7 +58,7 @@ def create_revocation_response(self, uri, http_method='POST', body=None, An invalid token type hint value is ignored by the authorization server and does not influence the revocation response. """ - headers = { + resp_headers = { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'Pragma': 'no-cache', @@ -73,8 +73,8 @@ def create_revocation_response(self, uri, http_method='POST', body=None, response_body = e.json if self.enable_jsonp and request.callback: response_body = '%s(%s);' % (request.callback, response_body) - headers.update(e.headers) - return headers, response_body, e.status_code + resp_headers.update(e.headers) + return resp_headers, response_body, e.status_code self.request_validator.revoke_token(request.token, request.token_type_hint, request) diff --git a/tests/oauth2/rfc6749/endpoints/test_client_authentication.py b/tests/oauth2/rfc6749/endpoints/test_client_authentication.py index 48c5f5ae..133da599 100644 --- a/tests/oauth2/rfc6749/endpoints/test_client_authentication.py +++ b/tests/oauth2/rfc6749/endpoints/test_client_authentication.py @@ -43,6 +43,11 @@ def setUp(self): token_generator=self.inspect_client) self.backend = BackendApplicationServer(self.validator, token_generator=self.inspect_client) + self.token_uri = 'http://example.com/path' + self.auth_uri = 'http://example.com/path?client_id=abc&response_type=token' + # should be base64 but no added value in this unittest + self.basicauth_client_creds = {"Authorization": "john:doe"} + self.basicauth_client_id = {"Authorization": "john:"} def set_client(self, request): request.client = mock.MagicMock() @@ -54,7 +59,9 @@ def set_client_id(self, client_id, request): request.client.client_id = 'mocked' return True - def set_username(self, username, password, client, request): + def basicauth_authenticate_client(self, request): + assert "Authorization" in request.headers + assert "john:doe" in request.headers["Authorization"] request.client = mock.MagicMock() request.client.client_id = 'mocked' return True @@ -86,6 +93,55 @@ def test_client_id_authentication(self): self.assertIn('Location', h) self.assertIn('access_token', get_fragment_credentials(h['Location'])) + def test_basicauth_web(self): + self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client + _, body, _ = self.web.create_token_response( + self.token_uri, + body='grant_type=authorization_code&code=mock', + headers=self.basicauth_client_creds + ) + self.assertIn('access_token', json.loads(body)) + + def test_basicauth_legacy(self): + self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client + _, body, _ = self.legacy.create_token_response( + self.token_uri, + body='grant_type=password&username=abc&password=secret', + headers=self.basicauth_client_creds + ) + self.assertIn('access_token', json.loads(body)) + + def test_basicauth_backend(self): + self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client + _, body, _ = self.backend.create_token_response( + self.token_uri, + body='grant_type=client_credentials', + headers=self.basicauth_client_creds + ) + self.assertIn('access_token', json.loads(body)) + + def test_basicauth_revoke(self): + self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client + + # legacy or any other uses the same RevocationEndpoint + _, body, status = self.legacy.create_revocation_response( + self.token_uri, + body='token=foobar', + headers=self.basicauth_client_creds + ) + self.assertEqual(status, 200, body) + + def test_basicauth_introspect(self): + self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client + + # legacy or any other uses the same IntrospectEndpoint + _, body, status = self.legacy.create_introspect_response( + self.token_uri, + body='token=foobar', + headers=self.basicauth_client_creds + ) + self.assertEqual(status, 200, body) + def test_custom_authentication(self): token_uri = 'http://example.com/path' diff --git a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py index f92652b8..b9bf76a5 100644 --- a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py @@ -123,7 +123,6 @@ def test_introspect_token_public_client_authentication_failed(self): self.assertEqual(loads(b)['error'], 'invalid_client') self.assertEqual(s, 401) - def test_introspect_unsupported_token(self): endpoint = IntrospectEndpoint(self.validator, supported_token_types=['access_token']) From 575638ce7ddb8727e08980235ccd82152af85703 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 24 Jan 2019 14:28:43 +0100 Subject: [PATCH 161/237] Bump to 3.0.1 --- oauthlib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index b37d288c..b23102c3 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,6 +12,6 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.0.0' +__version__ = '3.0.1' logging.getLogger('oauthlib').addHandler(NullHandler()) From 1dd58397d6a86fc930062cfecc6e2ea94e546aa7 Mon Sep 17 00:00:00 2001 From: Duane King Date: Thu, 24 Jan 2019 20:36:52 -0800 Subject: [PATCH 162/237] pep8 and docs --- oauthlib/oauth2/rfc6749/clients/base.py | 35 ++++++++++++++++--------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 1a50644e..9b05ad5d 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -28,8 +28,8 @@ 'Content-Type': 'application/x-www-form-urlencoded' } -class Client(object): +class Client(object): """Base OAuth2 client responsible for access token management. This class also acts as a generic interface providing methods common to all @@ -201,7 +201,7 @@ def add_token(self, uri, http_method='GET', body=None, headers=None, headers, token_placement, **kwargs) def prepare_authorization_request(self, authorization_url, state=None, - redirect_url=None, scope=None, **kwargs): + redirect_url=None, scope=None, **kwargs): """Prepare the authorization request. This is the first step in many OAuth flows in which the user is @@ -222,6 +222,8 @@ def prepare_authorization_request(self, authorization_url, state=None, the provider. If provided then it must also be provided in the token request. + :param scope: + :param kwargs: Additional parameters to included in the request. :returns: The prepared request tuple with (url, headers, body). @@ -233,12 +235,12 @@ def prepare_authorization_request(self, authorization_url, state=None, self.redirect_url = redirect_url or self.redirect_url self.scope = scope or self.scope auth_url = self.prepare_request_uri( - authorization_url, redirect_uri=self.redirect_url, - scope=self.scope, state=self.state, **kwargs) + authorization_url, redirect_uri=self.redirect_url, + scope=self.scope, state=self.state, **kwargs) return auth_url, FORM_ENC_HEADERS, '' def prepare_token_request(self, token_url, authorization_response=None, - redirect_url=None, state=None, body='', **kwargs): + redirect_url=None, state=None, body='', **kwargs): """Prepare a token creation request. Note that these requests usually require client authentication, either @@ -255,6 +257,8 @@ def prepare_token_request(self, token_url, authorization_response=None, :param redirect_url: The redirect_url supplied with the authorization request (if there was one). + :param state: + :param body: Existing request body (URL encoded string) to embed parameters into. This may contain extra paramters. Default ''. @@ -268,15 +272,15 @@ def prepare_token_request(self, token_url, authorization_response=None, state = state or self.state if authorization_response: self.parse_request_uri_response( - authorization_response, state=state) + authorization_response, state=state) self.redirect_url = redirect_url or self.redirect_url body = self.prepare_request_body(body=body, - redirect_uri=self.redirect_url, **kwargs) + redirect_uri=self.redirect_url, **kwargs) return token_url, FORM_ENC_HEADERS, body def prepare_refresh_token_request(self, token_url, refresh_token=None, - body='', scope=None, **kwargs): + body='', scope=None, **kwargs): """Prepare an access token refresh request. Expired access tokens can be replaced by new access tokens without @@ -304,11 +308,11 @@ def prepare_refresh_token_request(self, token_url, refresh_token=None, self.scope = scope or self.scope body = self.prepare_refresh_body(body=body, - refresh_token=refresh_token, scope=self.scope, **kwargs) + refresh_token=refresh_token, scope=self.scope, **kwargs) return token_url, FORM_ENC_HEADERS, body def prepare_token_revocation_request(self, revocation_url, token, - token_type_hint="access_token", body='', callback=None, **kwargs): + token_type_hint="access_token", body='', callback=None, **kwargs): """Prepare a token revocation request. :param revocation_url: Provider token revocation endpoint URL. @@ -319,6 +323,8 @@ def prepare_token_revocation_request(self, revocation_url, token, ``"refresh_token"``. This is optional and if you wish to not pass it you must provide ``token_type_hint=None``. + :param body: + :param callback: A jsonp callback such as ``package.callback`` to be invoked upon receiving the response. Not that it should not include a () suffix. @@ -363,8 +369,8 @@ def prepare_token_revocation_request(self, revocation_url, token, raise InsecureTransportError() return prepare_token_revocation_request(revocation_url, token, - token_type_hint=token_type_hint, body=body, callback=callback, - **kwargs) + token_type_hint=token_type_hint, body=body, callback=callback, + **kwargs) def parse_request_body_response(self, body, scope=None, **kwargs): """Parse the JSON response body. @@ -404,7 +410,7 @@ def parse_request_body_response(self, body, scope=None, **kwargs): If omitted, the authorization server SHOULD provide the expiration time via other means or document the default value. - **scope** + **scope** Providers may supply this in all responses but are required to only if it has changed since the authorization request. @@ -461,6 +467,9 @@ def _add_mac_token(self, uri, http_method='GET', body=None, Warning: MAC token support is experimental as the spec is not yet stable. """ + if token_placement != AUTH_HEADER: + raise ValueError("Invalid token placement.") + headers = tokens.prepare_mac_header(self.access_token, uri, self.mac_key, http_method, headers=headers, body=body, ext=ext, hash_algorithm=self.mac_algorithm, **kwargs) From e2562942279ad832f80b27e9629cf4d46a1c9bc5 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 31 Jan 2019 13:44:21 +0100 Subject: [PATCH 163/237] Add 3.0.1 changelog --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2cc0dd37..a5cb3244 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Changelog ========= +3.0.1 (2019-01-24) +------------------ +* Fixed OAuth2.0 regression introduced in 3.0.0: Revocation with Basic auth no longer possible #644 + 3.0.0 (2019-01-01) ------------------ OAuth2.0 Provider - outstanding Features From 0ef0a9c4342dfee4bd3aef7d6d9fa09e7226a732 Mon Sep 17 00:00:00 2001 From: Hoylen Sue Date: Tue, 19 Feb 2019 20:51:49 +1000 Subject: [PATCH 164/237] Fixed space encoding in base string URI used in the signature base string. --- CHANGELOG.rst | 5 ++++ oauthlib/oauth1/rfc5849/signature.py | 38 ++++++++++++++++++++----- tests/oauth1/rfc5849/test_signatures.py | 27 ++++++++++++------ 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2cc0dd37..8036614d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog ========= +TBD +--- + +* Fixed space encoding in base string URI used in the signature base string. + 3.0.0 (2019-01-01) ------------------ OAuth2.0 Provider - outstanding Features diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index e90d6f37..589b68ab 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -105,9 +105,9 @@ def construct_base_string(http_method, base_string_uri, return base_string -def normalize_base_string_uri(uri, host=None): +def base_string_uri(uri, host=None): """**Base String URI** - Per `section 3.4.1.2`_ of the spec. + Per `section 3.4.1.2`_ of RFC 5849. For example, the HTTP request:: @@ -177,7 +177,31 @@ def normalize_base_string_uri(uri, host=None): if (scheme, port) in default_ports: netloc = host - return urlparse.urlunparse((scheme, netloc, path, params, '', '')) + v = urlparse.urlunparse((scheme, netloc, path, params, '', '')) + + # RFC 5849 does not specify which characters are encoded in the + # "base string URI", nor how they are encoded - which is very bad, since + # the signatures won't match if there are any differences. Fortunately, + # most URIs only use characters that are clearly not encoded (e.g. digits + # and A-Z, a-z), so have avoided any differences between implementations. + # + # The example from its section 3.4.1.2 illustrates that spaces in + # the path are percent encoded. But it provides no guidance as to what other + # characters (if any) must be encoded (nor how); nor if characters in the + # other components are to be encoded or not. + # + # This implementation **assumes** that **only** the space is percent-encoded + # and it is done to the entire value (not just to spaces in the path). + # + # This code may need to be changed if it is discovered that other characters + # are expected to be encoded. + # + # Note: the "base string URI" returned by this function will be encoded + # again before being concatenated into the "signature base string". So any + # spaces in the URI will actually appear in the "signature base string" + # as "%2520" (the "%20" further encoded according to section 3.6). + + return v.replace(' ', '%20') # ** Request Parameters ** @@ -624,8 +648,8 @@ def verify_hmac_sha1(request, client_secret=None, """ norm_params = normalize_parameters(request.params) - uri = normalize_base_string_uri(request.uri) - base_string = construct_base_string(request.http_method, uri, norm_params) + bs_uri = base_string_uri(request.uri) + base_string = construct_base_string(request.http_method, bs_uri, norm_params) signature = sign_hmac_sha1(base_string, client_secret, resource_owner_secret) match = safe_string_equals(signature, request.signature) @@ -657,8 +681,8 @@ def verify_rsa_sha1(request, rsa_public_key): .. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2 """ norm_params = normalize_parameters(request.params) - uri = normalize_base_string_uri(request.uri) - message = construct_base_string(request.http_method, uri, norm_params).encode('utf-8') + bs_uri = base_string_uri(request.uri) + message = construct_base_string(request.http_method, bs_uri, norm_params).encode('utf-8') sig = binascii.a2b_base64(request.signature.encode('utf-8')) alg = _jwt_rs1_signing_algorithm() diff --git a/tests/oauth1/rfc5849/test_signatures.py b/tests/oauth1/rfc5849/test_signatures.py index 48609e5e..db2c5325 100644 --- a/tests/oauth1/rfc5849/test_signatures.py +++ b/tests/oauth1/rfc5849/test_signatures.py @@ -4,7 +4,7 @@ from oauthlib.common import unicode_type from oauthlib.oauth1.rfc5849.signature import (collect_parameters, construct_base_string, - normalize_base_string_uri, + base_string_uri, normalize_parameters, sign_hmac_sha1, sign_hmac_sha1_with_client, @@ -125,7 +125,7 @@ def test_construct_base_string(self): self.assertEqual(self.control_base_string, base_string) - def test_normalize_base_string_uri(self): + def test_base_string_uri(self): """ Example text to be turned into a normalized base string uri:: @@ -137,33 +137,44 @@ def test_normalize_base_string_uri(self): https://www.example.net:8080/ """ + # test first example from RFC 5849 section 3.4.1.2. + # Note: there is a space between "r" and "v" + uri = 'http://EXAMPLE.COM:80/r v/X?id=123' + self.assertEqual(base_string_uri(uri), + 'http://example.com/r%20v/X') + + # test second example from RFC 5849 section 3.4.1.2. + uri = 'https://www.example.net:8080/?q=1' + self.assertEqual(base_string_uri(uri), + 'https://www.example.net:8080/') + # test for unicode failure uri = b"www.example.com:8080" - self.assertRaises(ValueError, normalize_base_string_uri, uri) + self.assertRaises(ValueError, base_string_uri, uri) # test for missing scheme uri = "www.example.com:8080" - self.assertRaises(ValueError, normalize_base_string_uri, uri) + self.assertRaises(ValueError, base_string_uri, uri) # test a URI with the default port uri = "http://www.example.com:80/" - self.assertEqual(normalize_base_string_uri(uri), + self.assertEqual(base_string_uri(uri), "http://www.example.com/") # test a URI missing a path uri = "http://www.example.com" - self.assertEqual(normalize_base_string_uri(uri), + self.assertEqual(base_string_uri(uri), "http://www.example.com/") # test a relative URI uri = "/a-host-relative-uri" host = "www.example.com" - self.assertRaises(ValueError, normalize_base_string_uri, (uri, host)) + self.assertRaises(ValueError, base_string_uri, (uri, host)) # test overriding the URI's netloc with a host argument uri = "http://www.example.com/a-path" host = "alternatehost.example.com" - self.assertEqual(normalize_base_string_uri(uri, host), + self.assertEqual(base_string_uri(uri, host), "http://alternatehost.example.com/a-path") def test_collect_parameters(self): From 8c9f0a3cee9fab35fdf7269441daab666b931f59 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 20 Feb 2019 14:30:03 +0100 Subject: [PATCH 165/237] Fix 652: removed "state" from /token response. Fix OIDC /token flow where &state=None was always returned, and fix OAuth2.0 /token flow where &state=foobar was returned if &state=foobar was present in the token request. Remove "save_token" from create_token() signature cuz it was not used internally. Deprecated the option to let upstream libraries have a chance to remove it, if ever used. --- CHANGELOG.rst | 4 ++ .../rfc6749/grant_types/authorization_code.py | 4 +- .../rfc6749/grant_types/client_credentials.py | 3 +- .../oauth2/rfc6749/grant_types/implicit.py | 5 ++- .../rfc6749/grant_types/refresh_token.py | 3 +- .../resource_owner_password_credentials.py | 3 +- oauthlib/oauth2/rfc6749/tokens.py | 18 ++++----- .../openid/connect/core/grant_types/base.py | 3 -- .../connect/core/grant_types/implicit.py | 5 +++ oauthlib/openid/connect/core/tokens.py | 2 +- .../test_credentials_preservation.py | 12 ------ tests/oauth2/rfc6749/test_server.py | 39 +++++++++++++------ tests/openid/connect/core/test_server.py | 16 +++++--- 13 files changed, 68 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a5cb3244..9e0efdaf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Changelog ========= +TBD +------------------ +* #652: Fixed OIDC /token response which wrongly returned "&state=None" + 3.0.1 (2019-01-24) ------------------ * Fixed OAuth2.0 regression introduced in 3.0.0: Revocation with Basic auth no longer possible #644 diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 64633917..5f03d9cb 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -305,9 +305,11 @@ def create_token_response(self, request, token_handler): headers.update(e.headers) return headers, e.json, e.status_code - token = token_handler.create_token(request, refresh_token=self.refresh_token, save_token=False) + token = token_handler.create_token(request, refresh_token=self.refresh_token) + for modifier in self._token_modifiers: token = modifier(token, token_handler, request) + self.request_validator.save_token(token, request) self.request_validator.invalidate_authorization_code( request.client_id, request.code, request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py index c966795f..7e508577 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py @@ -76,10 +76,11 @@ def create_token_response(self, request, token_handler): headers.update(e.headers) return headers, e.json, e.status_code - token = token_handler.create_token(request, refresh_token=False, save_token=False) + token = token_handler.create_token(request, refresh_token=False) for modifier in self._token_modifiers: token = modifier(token) + self.request_validator.save_token(token, request) log.debug('Issuing token to client id %r (%r), %r.', diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py index d6de9061..48bae7a5 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py +++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py @@ -237,10 +237,13 @@ def create_token_response(self, request, token_handler): # "id_token token" - return the access token and the id token # "id_token" - don't return the access token if "token" in request.response_type.split(): - token = token_handler.create_token(request, refresh_token=False, save_token=False) + token = token_handler.create_token(request, refresh_token=False) else: token = {} + if request.state is not None: + token['state'] = request.state + for modifier in self._token_modifiers: token = modifier(token, token_handler, request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py index bd519e81..fc61d65c 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -64,10 +64,11 @@ def create_token_response(self, request, token_handler): return headers, e.json, e.status_code token = token_handler.create_token(request, - refresh_token=self.issue_new_refresh_tokens, save_token=False) + refresh_token=self.issue_new_refresh_tokens) for modifier in self._token_modifiers: token = modifier(token) + self.request_validator.save_token(token, request) log.debug('Issuing new token to client id %r (%r), %r.', diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py index f765d91b..5929afb4 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py @@ -104,10 +104,11 @@ def create_token_response(self, request, token_handler): headers.update(e.headers) return headers, e.json, e.status_code - token = token_handler.create_token(request, self.refresh_token, save_token=False) + token = token_handler.create_token(request, self.refresh_token) for modifier in self._token_modifiers: token = modifier(token) + self.request_validator.save_token(token, request) log.debug('Issuing token %r to client id %r (%r) and username %s.', diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index d78df09e..44a9a977 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -12,6 +12,7 @@ import hashlib import hmac from binascii import b2a_base64 +import warnings from oauthlib import common from oauthlib.common import add_params_to_qs, add_params_to_uri, unicode_type @@ -296,15 +297,18 @@ def __init__(self, request_validator=None, token_generator=None, ) self.expires_in = expires_in or 3600 - def create_token(self, request, refresh_token=False, save_token=True): + def create_token(self, request, refresh_token=False, **kwargs): """ Create a BearerToken, by default without refresh token. - + :param request: OAuthlib request. :type request: oauthlib.common.Request :param refresh_token: - :param save_token: """ + if "save_token" in kwargs: + warnings.warn("`save_token` has been deprecated, it was not used internally." + "If you do, use `request_validator.save_token()` instead.", + DeprecationWarning) if callable(self.expires_in): expires_in = self.expires_in(request) @@ -325,9 +329,6 @@ def create_token(self, request, refresh_token=False, save_token=True): if request.scopes is not None: token['scope'] = ' '.join(request.scopes) - if request.state is not None: - token['state'] = request.state - if refresh_token: if (request.refresh_token and not self.request_validator.rotate_refresh_token(request)): @@ -336,10 +337,7 @@ def create_token(self, request, refresh_token=False, save_token=True): token['refresh_token'] = self.refresh_token_generator(request) token.update(request.extra_credentials or {}) - token = OAuth2Token(token) - if save_token: - self.request_validator.save_bearer_token(token, request) - return token + return OAuth2Token(token) def validate_request(self, request): """ diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index fa578a57..05cdd376 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -58,9 +58,6 @@ def add_id_token(self, token, token_handler, request): if request.response_type and 'id_token' not in request.response_type: return token - if 'state' not in token: - token['state'] = request.state - if request.max_age: d = datetime.datetime.utcnow() token['auth_time'] = d.isoformat("T") + "Z" diff --git a/oauthlib/openid/connect/core/grant_types/implicit.py b/oauthlib/openid/connect/core/grant_types/implicit.py index 0eaa5b38..0a6fcb7f 100644 --- a/oauthlib/openid/connect/core/grant_types/implicit.py +++ b/oauthlib/openid/connect/core/grant_types/implicit.py @@ -26,3 +26,8 @@ def __init__(self, request_validator=None, **kwargs): self.custom_validators.post_auth.append( self.openid_implicit_authorization_validator) self.register_token_modifier(self.add_id_token) + + def add_id_token(self, token, token_handler, request): + if 'state' not in token: + token['state'] = request.state + return super(ImplicitGrant, self).add_id_token(token, token_handler, request) diff --git a/oauthlib/openid/connect/core/tokens.py b/oauthlib/openid/connect/core/tokens.py index 6b68891a..b67cdf2a 100644 --- a/oauthlib/openid/connect/core/tokens.py +++ b/oauthlib/openid/connect/core/tokens.py @@ -25,7 +25,7 @@ def __init__(self, request_validator=None, token_generator=None, ) self.expires_in = expires_in or 3600 - def create_token(self, request, refresh_token=False, save_token=False): + def create_token(self, request, refresh_token=False): """Create a JWT Token, using requestvalidator method.""" if callable(self.expires_in): diff --git a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py index 1a2f66bc..c77d18e9 100644 --- a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py +++ b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py @@ -42,18 +42,6 @@ def set_client(self, request): def test_state_preservation(self): auth_uri = 'http://example.com/path?state=xyz&client_id=abc&response_type=' - token_uri = 'http://example.com/path' - - # authorization grant - h, _, s = self.web.create_authorization_response( - auth_uri + 'code', scopes=['random']) - self.assertEqual(s, 302) - self.assertIn('Location', h) - code = get_query_credentials(h['Location'])['code'][0] - self.validator.validate_code.side_effect = self.set_state('xyz') - _, body, _ = self.web.create_token_response(token_uri, - body='grant_type=authorization_code&code=%s' % code) - self.assertEqual(json.loads(body)['state'], 'xyz') # implicit grant h, _, s = self.mobile.create_authorization_response( diff --git a/tests/oauth2/rfc6749/test_server.py b/tests/oauth2/rfc6749/test_server.py index b623a9ba..2c6ecff1 100644 --- a/tests/oauth2/rfc6749/test_server.py +++ b/tests/oauth2/rfc6749/test_server.py @@ -144,7 +144,7 @@ def set_user(request): @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): - body = 'grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' + body = 'grant_type=authorization_code&code=abc&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( '', body=body) token = { @@ -152,23 +152,27 @@ def test_authorization_grant(self): 'expires_in': self.expires_in, 'access_token': 'abc', 'refresh_token': 'abc', - 'scope': 'all of them', - 'state': 'xyz' + 'scope': 'all of them' } self.assertEqual(json.loads(body), token) - body = 'grant_type=authorization_code&code=abc&state=xyz' + body = 'grant_type=authorization_code&code=abc' headers, body, status_code = self.endpoint.create_token_response( '', body=body) token = { 'token_type': 'Bearer', 'expires_in': self.expires_in, 'access_token': 'abc', - 'refresh_token': 'abc', - 'state': 'xyz' + 'refresh_token': 'abc' } self.assertEqual(json.loads(body), token) + # try with additional custom variables + body = 'grant_type=authorization_code&code=abc&state=foobar' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + self.assertEqual(json.loads(body), token) + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_password_grant(self): body = 'grant_type=password&username=a&password=hello&scope=all+of+them' @@ -277,7 +281,7 @@ def set_user(request): @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): - body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' + body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( '', body=body) body = json.loads(body) @@ -286,12 +290,11 @@ def test_authorization_grant(self): 'expires_in': self.expires_in, 'access_token': body['access_token'], 'refresh_token': 'abc', - 'scope': 'all of them', - 'state': 'xyz' + 'scope': 'all of them' } self.assertEqual(body, token) - body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&state=xyz' + body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc' headers, body, status_code = self.endpoint.create_token_response( '', body=body) body = json.loads(body) @@ -299,8 +302,20 @@ def test_authorization_grant(self): 'token_type': 'Bearer', 'expires_in': self.expires_in, 'access_token': body['access_token'], - 'refresh_token': 'abc', - 'state': 'xyz' + 'refresh_token': 'abc' + } + self.assertEqual(body, token) + + # try with additional custom variables + body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&state=foobar' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + body = json.loads(body) + token = { + 'token_type': 'Bearer', + 'expires_in': self.expires_in, + 'access_token': body['access_token'], + 'refresh_token': 'abc' } self.assertEqual(body, token) diff --git a/tests/openid/connect/core/test_server.py b/tests/openid/connect/core/test_server.py index ffab7b0b..756c9d0f 100644 --- a/tests/openid/connect/core/test_server.py +++ b/tests/openid/connect/core/test_server.py @@ -143,7 +143,7 @@ def set_user(request): @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): - body = 'grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' + body = 'grant_type=authorization_code&code=abc&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( '', body=body) token = { @@ -151,23 +151,27 @@ def test_authorization_grant(self): 'expires_in': self.expires_in, 'access_token': 'abc', 'refresh_token': 'abc', - 'scope': 'all of them', - 'state': 'xyz' + 'scope': 'all of them' } self.assertEqual(json.loads(body), token) - body = 'grant_type=authorization_code&code=abc&state=xyz' + body = 'grant_type=authorization_code&code=abc' headers, body, status_code = self.endpoint.create_token_response( '', body=body) token = { 'token_type': 'Bearer', 'expires_in': self.expires_in, 'access_token': 'abc', - 'refresh_token': 'abc', - 'state': 'xyz' + 'refresh_token': 'abc' } self.assertEqual(json.loads(body), token) + # ignore useless fields + body = 'grant_type=authorization_code&code=abc&state=foobar' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + self.assertEqual(json.loads(body), token) + def test_missing_type(self): _, body, _ = self.endpoint.create_token_response('', body='') token = {'error': 'unsupported_grant_type'} From 58f1c3fe4020d13d4c2f7b80902b2c157fde807d Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 21 Feb 2019 10:01:29 +0100 Subject: [PATCH 166/237] Add clarity to the deprecation warning --- oauthlib/oauth2/rfc6749/tokens.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index 44a9a977..79739238 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -306,8 +306,8 @@ def create_token(self, request, refresh_token=False, **kwargs): :param refresh_token: """ if "save_token" in kwargs: - warnings.warn("`save_token` has been deprecated, it was not used internally." - "If you do, use `request_validator.save_token()` instead.", + warnings.warn("`save_token` has been deprecated, it was not called internally." + "If you do, call `request_validator.save_token()` instead.", DeprecationWarning) if callable(self.expires_in): From 4205dc1b4240e30d966c3fd4fe872f83413b2e2c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 21 Feb 2019 10:16:55 +0100 Subject: [PATCH 167/237] Add authorization "state" preservation back for AuthCode --- .../rfc6749/endpoints/test_credentials_preservation.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py index c77d18e9..c0cf86d8 100644 --- a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py +++ b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py @@ -43,6 +43,13 @@ def set_client(self, request): def test_state_preservation(self): auth_uri = 'http://example.com/path?state=xyz&client_id=abc&response_type=' + # authorization grant + h, _, s = self.web.create_authorization_response( + auth_uri + 'code', scopes=['random']) + self.assertEqual(s, 302) + self.assertIn('Location', h) + self.assertEqual(get_query_credentials(h['Location'])['state'][0], 'xyz') + # implicit grant h, _, s = self.mobile.create_authorization_response( auth_uri + 'token', scopes=['random']) From 2904de612a5e52c14776978dd5a31cdde2bfc34e Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 21 Feb 2019 10:17:23 +0100 Subject: [PATCH 168/237] Removed useless set_state internal function Does not have purpose for /token request --- .../rfc6749/endpoints/test_credentials_preservation.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py index c0cf86d8..e7c66b67 100644 --- a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py +++ b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py @@ -29,12 +29,6 @@ def setUp(self): self.web = WebApplicationServer(self.validator) self.mobile = MobileApplicationServer(self.validator) - def set_state(self, state): - def set_request_state(client_id, code, client, request): - request.state = state - return True - return set_request_state - def set_client(self, request): request.client = mock.MagicMock() request.client.client_id = 'mocked' @@ -128,7 +122,7 @@ def test_default_uri_in_token(self): # was not given in the authorization AND not in the token request. self.validator.confirm_redirect_uri.return_value = True code = get_query_credentials(h['Location'])['code'][0] - self.validator.validate_code.side_effect = self.set_state('xyz') + self.validator.validate_code.return_value = True _, body, s = self.web.create_token_response(token_uri, body='grant_type=authorization_code&code=%s' % code) self.assertEqual(s, 200) From 0a0a718355354f621e475b8bd8162b726d838c11 Mon Sep 17 00:00:00 2001 From: Hoylen Sue Date: Thu, 21 Feb 2019 20:51:45 +1000 Subject: [PATCH 169/237] Renamed normalize_base_string_uri to base_string_uri. --- oauthlib/oauth1/rfc5849/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/oauthlib/oauth1/rfc5849/__init__.py b/oauthlib/oauth1/rfc5849/__init__.py index 7313286c..d6c44ea7 100644 --- a/oauthlib/oauth1/rfc5849/__init__.py +++ b/oauthlib/oauth1/rfc5849/__init__.py @@ -133,8 +133,7 @@ def get_oauth_signature(self, request): log.debug("Collected params: {0}".format(collected_params)) normalized_params = signature.normalize_parameters(collected_params) - normalized_uri = signature.normalize_base_string_uri(uri, - headers.get('Host', None)) + normalized_uri = signature.base_string_uri(uri, headers.get('Host', None)) log.debug("Normalized params: {0}".format(normalized_params)) log.debug("Normalized URI: {0}".format(normalized_uri)) From 42023d8303113073e31a57e1bbf70216b7120e20 Mon Sep 17 00:00:00 2001 From: Hoylen Sue Date: Fri, 22 Feb 2019 09:32:24 +1000 Subject: [PATCH 170/237] Renamed construct_base_string to signature_base_string. --- oauthlib/oauth1/rfc5849/__init__.py | 2 +- oauthlib/oauth1/rfc5849/signature.py | 23 ++++++++++++++--------- tests/oauth1/rfc5849/test_signatures.py | 12 ++++++------ 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/oauthlib/oauth1/rfc5849/__init__.py b/oauthlib/oauth1/rfc5849/__init__.py index d6c44ea7..4f462bbc 100644 --- a/oauthlib/oauth1/rfc5849/__init__.py +++ b/oauthlib/oauth1/rfc5849/__init__.py @@ -137,7 +137,7 @@ def get_oauth_signature(self, request): log.debug("Normalized params: {0}".format(normalized_params)) log.debug("Normalized URI: {0}".format(normalized_uri)) - base_string = signature.construct_base_string(request.http_method, + base_string = signature.signature_base_string(request.http_method, normalized_uri, normalized_params) log.debug("Signing: signature base string: {0}".format(base_string)) diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index 589b68ab..f899aca8 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -40,9 +40,10 @@ log = logging.getLogger(__name__) -def construct_base_string(http_method, base_string_uri, + +def signature_base_string(http_method, base_str_uri, normalized_encoded_request_parameters): - """**String Construction** + """**Construct the signature base string.** Per `section 3.4.1.1`_ of the spec. For example, the HTTP request:: @@ -90,7 +91,7 @@ def construct_base_string(http_method, base_string_uri, # # .. _`Section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2 # .. _`Section 3.4.6`: https://tools.ietf.org/html/rfc5849#section-3.4.6 - base_string += utils.escape(base_string_uri) + base_string += utils.escape(base_str_uri) # 4. An "&" character (ASCII code 38). base_string += '&' @@ -649,12 +650,14 @@ def verify_hmac_sha1(request, client_secret=None, """ norm_params = normalize_parameters(request.params) bs_uri = base_string_uri(request.uri) - base_string = construct_base_string(request.http_method, bs_uri, norm_params) - signature = sign_hmac_sha1(base_string, client_secret, + sig_base_str = signature_base_string(request.http_method, bs_uri, + norm_params) + signature = sign_hmac_sha1(sig_base_str, client_secret, resource_owner_secret) match = safe_string_equals(signature, request.signature) if not match: - log.debug('Verify HMAC-SHA1 failed: sig base string: %s', base_string) + log.debug('Verify HMAC-SHA1 failed: signature base string: %s', + sig_base_str) return match @@ -682,15 +685,17 @@ def verify_rsa_sha1(request, rsa_public_key): """ norm_params = normalize_parameters(request.params) bs_uri = base_string_uri(request.uri) - message = construct_base_string(request.http_method, bs_uri, norm_params).encode('utf-8') + sig_base_str = signature_base_string(request.http_method, bs_uri, + norm_params).encode('utf-8') sig = binascii.a2b_base64(request.signature.encode('utf-8')) alg = _jwt_rs1_signing_algorithm() key = _prepare_key_plus(alg, rsa_public_key) - verify_ok = alg.verify(message, key, sig) + verify_ok = alg.verify(sig_base_str, key, sig) if not verify_ok: - log.debug('Verify RSA-SHA1 failed: sig base string: %s', message) + log.debug('Verify RSA-SHA1 failed: signature base string: %s', + sig_base_str) return verify_ok diff --git a/tests/oauth1/rfc5849/test_signatures.py b/tests/oauth1/rfc5849/test_signatures.py index db2c5325..bb0dc78b 100644 --- a/tests/oauth1/rfc5849/test_signatures.py +++ b/tests/oauth1/rfc5849/test_signatures.py @@ -3,7 +3,7 @@ from oauthlib.common import unicode_type from oauthlib.oauth1.rfc5849.signature import (collect_parameters, - construct_base_string, + signature_base_string, base_string_uri, normalize_parameters, sign_hmac_sha1, @@ -79,7 +79,7 @@ def setUp(self): resource_owner_secret = self.resource_owner_secret ) - def test_construct_base_string(self): + def test_signature_base_string(self): """ Example text to be turned into a base string:: @@ -104,20 +104,20 @@ def test_construct_base_string(self): D%2522137131201%2522%252Coauth_nonce%253D%25227d8f3e4a%2522%252Coau th_signature%253D%2522bYT5CMsGcbgUdFHObYMEfcx6bsw%25253D%2522 """ - self.assertRaises(ValueError, construct_base_string, + self.assertRaises(ValueError, signature_base_string, self.http_method, self.base_string_url, self.normalized_encoded_request_parameters) - self.assertRaises(ValueError, construct_base_string, + self.assertRaises(ValueError, signature_base_string, self.http_method.decode('utf-8'), self.base_string_url, self.normalized_encoded_request_parameters) - self.assertRaises(ValueError, construct_base_string, + self.assertRaises(ValueError, signature_base_string, self.http_method.decode('utf-8'), self.base_string_url.decode('utf-8'), self.normalized_encoded_request_parameters) - base_string = construct_base_string( + base_string = signature_base_string( self.http_method.decode('utf-8'), self.base_string_url.decode('utf-8'), self.normalized_encoded_request_parameters.decode('utf-8') From 54db1bfd65d1d17d1d45c12c8626b9e7fa84e694 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 22 Feb 2019 11:12:49 +0100 Subject: [PATCH 171/237] Remove usage of "state" for code/token response. --- examples/skeleton_oauth2_web_application_server.py | 6 +++--- oauthlib/oauth2/rfc6749/request_validator.py | 8 +------- .../connect/core/grant_types/test_authorization_code.py | 1 - 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/examples/skeleton_oauth2_web_application_server.py b/examples/skeleton_oauth2_web_application_server.py index e53232f3..9a303735 100644 --- a/examples/skeleton_oauth2_web_application_server.py +++ b/examples/skeleton_oauth2_web_application_server.py @@ -48,7 +48,7 @@ def validate_response_type(self, client_id, response_type, client, request, *arg def save_authorization_code(self, client_id, code, request, *args, **kwargs): # Remember to associate it with request.scopes, request.redirect_uri - # request.client, request.state and request.user (the last is passed in + # request.client and request.user (the last is passed in # post_authorization credentials, i.e. { 'user': request.user}. pass @@ -63,8 +63,8 @@ def authenticate_client_id(self, client_id, request, *args, **kwargs): return False def validate_code(self, client_id, code, client, request, *args, **kwargs): - # Validate the code belongs to the client. Add associated scopes, - # state and user to request.scopes and request.user. + # Validate the code belongs to the client. Add associated scopes + # and user to request.scopes and request.user. pass def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request, *args, **kwargs): diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 193a9e1f..5ff30d80 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -266,7 +266,6 @@ def save_authorization_code(self, client_id, code, request, *args, **kwargs): - the redirect URI used (``request.redirect_uri``) - a resource owner / user (``request.user``) - the authorized scopes (``request.scopes``) - - the client state, if given (``code.get('state')``) To support PKCE, you MUST associate the code with: - Code Challenge (``request.code_challenge``) and @@ -277,10 +276,6 @@ def save_authorization_code(self, client_id, code, request, *args, **kwargs): ``{'code': 'sdf345jsdf0934f'}`` - It may also have a ``state`` key containing a nonce for the client, if it - chose to send one. That value should be saved and used in - ``.validate_code``. - It may also have a ``claims`` parameter which, when present, will be a dict deserialized from JSON as described at http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter @@ -352,7 +347,7 @@ def save_bearer_token(self, token, request, *args, **kwargs): 'expires_in': 3600, 'scope': 'string of space separated authorized scopes', 'refresh_token': '23sdf876234', # if issued - 'state': 'given_by_client', # if supplied by client + 'state': 'given_by_client', # if supplied by client (implicit ONLY) } Note that while "scope" is a string-separated list of authorized scopes, @@ -559,7 +554,6 @@ def validate_code(self, client_id, code, client, request, *args, **kwargs): with the code in 'save_authorization_code': - request.user - - request.state (if given) - request.scopes - request.claims (if given) OBS! The request.user attribute should be set to the resource owner diff --git a/tests/openid/connect/core/grant_types/test_authorization_code.py b/tests/openid/connect/core/grant_types/test_authorization_code.py index c3c78242..fbbd5ffd 100644 --- a/tests/openid/connect/core/grant_types/test_authorization_code.py +++ b/tests/openid/connect/core/grant_types/test_authorization_code.py @@ -116,7 +116,6 @@ def test_no_prompt_authorization(self, generate_token): def set_scopes(self, client_id, code, client, request): request.scopes = self.request.scopes - request.state = self.request.state request.user = 'bob' return True From ff67daa5b3c9640a1e4754a949584c9315dbf5cd Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 15:36:13 +0100 Subject: [PATCH 172/237] Change to 3.0.2-dev as long as master is in "dev" --- CHANGELOG.rst | 2 +- oauthlib/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f49fb928..ade6243b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -TBD +3.0.2 (TBD) ------------------ * #650 Fixed space encoding in base string URI used in the signature base string. * #652: Fixed OIDC /token response which wrongly returned "&state=None" diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index b23102c3..8eb82a65 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,6 +12,6 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.0.1' +__version__ = '3.0.2-dev' logging.getLogger('oauthlib').addHandler(NullHandler()) From aee1bb88135090202ebdfc5974c16730b52bc5e7 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 20:57:56 +0100 Subject: [PATCH 173/237] OIDC: Raise error=invalid_request when nonce is mandatory Until now, only OIDC implicit was raising an error, but OIDC hybrid contain a couple of mandatory nonce, too. --- .../openid/connect/core/grant_types/base.py | 23 ------- .../openid/connect/core/grant_types/hybrid.py | 25 ++++++++ .../connect/core/grant_types/implicit.py | 23 ++++++- .../connect/core/grant_types/test_implicit.py | 60 ++++++++++++++++--- 4 files changed, 99 insertions(+), 32 deletions(-) diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index 05cdd376..4f5c9445 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -247,28 +247,5 @@ def openid_authorization_validator(self, request): return request_info - def openid_implicit_authorization_validator(self, request): - """Additional validation when following the implicit flow. - """ - # Undefined in OpenID Connect, fall back to OAuth2 definition. - if request.response_type == 'token': - return {} - - # Treat it as normal OAuth 2 auth code request if openid is not present - if not request.scopes or 'openid' not in request.scopes: - return {} - - # REQUIRED. String value used to associate a Client session with an ID - # Token, and to mitigate replay attacks. The value is passed through - # unmodified from the Authentication Request to the ID Token. - # Sufficient entropy MUST be present in the nonce values used to - # prevent attackers from guessing values. For implementation notes, see - # Section 15.5.2. - if not request.nonce: - desc = 'Request is missing mandatory nonce parameter.' - raise InvalidRequestError(request=request, description=desc) - - return {} - OpenIDConnectBase = GrantTypeBase diff --git a/oauthlib/openid/connect/core/grant_types/hybrid.py b/oauthlib/openid/connect/core/grant_types/hybrid.py index 54669ae4..685fa08a 100644 --- a/oauthlib/openid/connect/core/grant_types/hybrid.py +++ b/oauthlib/openid/connect/core/grant_types/hybrid.py @@ -8,6 +8,7 @@ import logging from oauthlib.oauth2.rfc6749.grant_types.authorization_code import AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant +from oauthlib.oauth2.rfc6749.errors import InvalidRequestError from .base import GrantTypeBase from ..request_validator import RequestValidator @@ -34,3 +35,27 @@ def __init__(self, request_validator=None, **kwargs): self.register_code_modifier(self.add_token) self.register_code_modifier(self.add_id_token) self.register_token_modifier(self.add_id_token) + + def openid_authorization_validator(self, request): + """Additional validation when following the Authorization Code flow. + """ + request_info = super(HybridGrant, self).openid_authorization_validator(request) + if not request_info: # returns immediately if OAuth2.0 + return request_info + + # REQUIRED if the Response Type of the request is `code + # id_token` or `code id_token token` and OPTIONAL when the + # Response Type of the request is `code token`. It is a string + # value used to associate a Client session with an ID Token, + # and to mitigate replay attacks. The value is passed through + # unmodified from the Authentication Request to the ID + # Token. Sufficient entropy MUST be present in the `nonce` + # values used to prevent attackers from guessing values. For + # implementation notes, see Section 15.5.2. + if request.response_type in ["code id_token", "code id_token token"]: + if not request.nonce: + raise InvalidRequestError( + request=request, + description='Request is missing mandatory nonce parameter.' + ) + return request_info diff --git a/oauthlib/openid/connect/core/grant_types/implicit.py b/oauthlib/openid/connect/core/grant_types/implicit.py index 0a6fcb7f..d3797b28 100644 --- a/oauthlib/openid/connect/core/grant_types/implicit.py +++ b/oauthlib/openid/connect/core/grant_types/implicit.py @@ -10,6 +10,7 @@ from .base import GrantTypeBase from oauthlib.oauth2.rfc6749.grant_types.implicit import ImplicitGrant as OAuth2ImplicitGrant +from oauthlib.oauth2.rfc6749.errors import InvalidRequestError log = logging.getLogger(__name__) @@ -23,11 +24,29 @@ def __init__(self, request_validator=None, **kwargs): self.register_response_type('id_token token') self.custom_validators.post_auth.append( self.openid_authorization_validator) - self.custom_validators.post_auth.append( - self.openid_implicit_authorization_validator) self.register_token_modifier(self.add_id_token) def add_id_token(self, token, token_handler, request): if 'state' not in token: token['state'] = request.state return super(ImplicitGrant, self).add_id_token(token, token_handler, request) + + def openid_authorization_validator(self, request): + """Additional validation when following the implicit flow. + """ + request_info = super(ImplicitGrant, self).openid_authorization_validator(request) + if not request_info: # returns immediately if OAuth2.0 + return request_info + + # REQUIRED. String value used to associate a Client session with an ID + # Token, and to mitigate replay attacks. The value is passed through + # unmodified from the Authentication Request to the ID Token. + # Sufficient entropy MUST be present in the nonce values used to + # prevent attackers from guessing values. For implementation notes, see + # Section 15.5.2. + if not request.nonce: + raise InvalidRequestError( + request=request, + description='Request is missing mandatory nonce parameter.' + ) + return request_info diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py index 7ab198ac..54fd8b9d 100644 --- a/tests/openid/connect/core/grant_types/test_implicit.py +++ b/tests/openid/connect/core/grant_types/test_implicit.py @@ -4,6 +4,7 @@ import mock from oauthlib.common import Request +from oauthlib.oauth2.rfc6749 import errors from oauthlib.oauth2.rfc6749.tokens import BearerToken from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant @@ -30,8 +31,8 @@ def setUp(self): self.request.client_id = 'abcdef' self.request.response_type = 'id_token token' self.request.redirect_uri = 'https://a.b/cb' - self.request.nonce = 'zxc' self.request.state = 'abc' + self.request.nonce = 'xyz' self.mock_validator = mock.MagicMock() self.mock_validator.get_id_token.side_effect = get_id_token_mock @@ -61,12 +62,6 @@ def test_authorization(self, generate_token): self.assertEqual(b, None) self.assertEqual(s, 302) - self.request.nonce = None - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) - @mock.patch('oauthlib.common.generate_token') def test_no_prompt_authorization(self, generate_token): generate_token.return_value = 'abc' @@ -105,16 +100,41 @@ def test_no_prompt_authorization(self, generate_token): h, b, s = self.auth.create_authorization_response(self.request, bearer) self.assertIn('error=login_required', h['Location']) + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): def setUp(self): super(OpenIDHybridCodeTokenTest, self).setUp() self.request.response_type = 'code token' + self.request.nonce = None self.auth = HybridGrant(request_validator=self.mock_validator) self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' + @mock.patch('oauthlib.common.generate_token') + def test_optional_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = 'xyz' + scope, info = self.auth.validate_authorization_request(self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) + self.assertEqual(b, None) + self.assertEqual(s, 302) + class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): @@ -122,11 +142,24 @@ def setUp(self): super(OpenIDHybridCodeIdTokenTest, self).setUp() self.mock_validator.get_code_challenge.return_value = None self.request.response_type = 'code id_token' + self.request.nonce = 'zxc' self.auth = HybridGrant(request_validator=self.mock_validator) token = 'MOCKED_TOKEN' self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): @@ -134,7 +167,20 @@ def setUp(self): super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() self.mock_validator.get_code_challenge.return_value = None self.request.response_type = 'code id_token token' + self.request.nonce = 'xyz' self.auth = HybridGrant(request_validator=self.mock_validator) token = 'MOCKED_TOKEN' self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) From 39f213b2106d079ce371f541e180ac4cd685d4e3 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:34:31 +0100 Subject: [PATCH 174/237] Add nonce auth request check for authorization_code --- .../core/grant_types/test_authorization_code.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/openid/connect/core/grant_types/test_authorization_code.py b/tests/openid/connect/core/grant_types/test_authorization_code.py index fbbd5ffd..b721a19e 100644 --- a/tests/openid/connect/core/grant_types/test_authorization_code.py +++ b/tests/openid/connect/core/grant_types/test_authorization_code.py @@ -40,6 +40,7 @@ def setUp(self): self.request.grant_type = 'authorization_code' self.request.redirect_uri = 'https://a.b/cb' self.request.state = 'abc' + self.request.nonce = None self.mock_validator = mock.MagicMock() self.mock_validator.authenticate_client.side_effect = self.set_client @@ -147,3 +148,16 @@ def test_create_token_response(self): self.assertIn('scope', token) self.assertNotIn('id_token', token) self.assertNotIn('openid', token['scope']) + + @mock.patch('oauthlib.common.generate_token') + def test_optional_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = 'xyz' + scope, info = self.auth.validate_authorization_request(self.request) + + bearer = BearerToken(self.mock_validator) + self.request.response_mode = 'query' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_query) + self.assertEqual(b, None) + self.assertEqual(s, 302) From c76db93ed3b20295a04ef58f0048ef53cee9714c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:34:48 +0100 Subject: [PATCH 175/237] Add nonce mandatory check for "id_token" response_type --- .../connect/core/grant_types/test_implicit.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py index 54fd8b9d..948edd34 100644 --- a/tests/openid/connect/core/grant_types/test_implicit.py +++ b/tests/openid/connect/core/grant_types/test_implicit.py @@ -113,6 +113,27 @@ def test_required_nonce(self, generate_token): self.assertEqual(s, 302) +class OpenIDImplicitNoAccessTokenTest(OpenIDImplicitTest): + def setUp(self): + super(OpenIDImplicitNoAccessTokenTest, self).setUp() + self.request.response_type = 'id_token' + token = 'MOCKED_TOKEN' + self.url_query = 'https://a.b/cb?state=abc&id_token=%s' % token + self.url_fragment = 'https://a.b/cb#state=abc&id_token=%s' % token + + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): def setUp(self): From 512de2c2b0be394dbe873c39b7ef085a665cdc14 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:36:14 +0100 Subject: [PATCH 176/237] Move HybridGrant test into its respective file. --- .../connect/core/grant_types/test_hybrid.py | 75 ++++++++++++++++++ .../connect/core/grant_types/test_implicit.py | 76 +------------------ 2 files changed, 76 insertions(+), 75 deletions(-) diff --git a/tests/openid/connect/core/grant_types/test_hybrid.py b/tests/openid/connect/core/grant_types/test_hybrid.py index 6eb80370..8964053e 100644 --- a/tests/openid/connect/core/grant_types/test_hybrid.py +++ b/tests/openid/connect/core/grant_types/test_hybrid.py @@ -4,6 +4,8 @@ from tests.oauth2.rfc6749.grant_types.test_authorization_code import \ AuthorizationCodeGrantTest +from .test_authorization_code import OpenIDAuthCodeTest + class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): @@ -12,3 +14,76 @@ class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): def setUp(self): super(OpenIDHybridInterferenceTest, self).setUp() self.auth = HybridGrant(request_validator=self.mock_validator) + + +class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeTokenTest, self).setUp() + self.request.response_type = 'code token' + self.request.nonce = None + self.auth = HybridGrant(request_validator=self.mock_validator) + self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' + self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' + + @mock.patch('oauthlib.common.generate_token') + def test_optional_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = 'xyz' + scope, info = self.auth.validate_authorization_request(self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + +class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeIdTokenTest, self).setUp() + self.mock_validator.get_code_challenge.return_value = None + self.request.response_type = 'code id_token' + self.request.nonce = 'zxc' + self.auth = HybridGrant(request_validator=self.mock_validator) + token = 'MOCKED_TOKEN' + self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token + self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token + + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + +class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() + self.mock_validator.get_code_challenge.return_value = None + self.request.response_type = 'code id_token token' + self.request.nonce = 'xyz' + self.auth = HybridGrant(request_validator=self.mock_validator) + token = 'MOCKED_TOKEN' + self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py index 948edd34..1ee805c1 100644 --- a/tests/openid/connect/core/grant_types/test_implicit.py +++ b/tests/openid/connect/core/grant_types/test_implicit.py @@ -7,11 +7,10 @@ from oauthlib.oauth2.rfc6749 import errors from oauthlib.oauth2.rfc6749.tokens import BearerToken from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt -from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant from tests.oauth2.rfc6749.grant_types.test_implicit import ImplicitGrantTest from tests.unittest import TestCase -from .test_authorization_code import get_id_token_mock, OpenIDAuthCodeTest +from .test_authorization_code import get_id_token_mock class OpenIDImplicitInterferenceTest(ImplicitGrantTest): @@ -132,76 +131,3 @@ def test_required_nonce(self, generate_token): self.assertIn('error=invalid_request', h['Location']) self.assertEqual(b, None) self.assertEqual(s, 302) - - -class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeTokenTest, self).setUp() - self.request.response_type = 'code token' - self.request.nonce = None - self.auth = HybridGrant(request_validator=self.mock_validator) - self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' - self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' - - @mock.patch('oauthlib.common.generate_token') - def test_optional_nonce(self, generate_token): - generate_token.return_value = 'abc' - self.request.nonce = 'xyz' - scope, info = self.auth.validate_authorization_request(self.request) - - bearer = BearerToken(self.mock_validator) - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - -class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeIdTokenTest, self).setUp() - self.mock_validator.get_code_challenge.return_value = None - self.request.response_type = 'code id_token' - self.request.nonce = 'zxc' - self.auth = HybridGrant(request_validator=self.mock_validator) - token = 'MOCKED_TOKEN' - self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token - self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token - - @mock.patch('oauthlib.common.generate_token') - def test_required_nonce(self, generate_token): - generate_token.return_value = 'abc' - self.request.nonce = None - self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) - - bearer = BearerToken(self.mock_validator) - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - -class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() - self.mock_validator.get_code_challenge.return_value = None - self.request.response_type = 'code id_token token' - self.request.nonce = 'xyz' - self.auth = HybridGrant(request_validator=self.mock_validator) - token = 'MOCKED_TOKEN' - self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token - self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token - - @mock.patch('oauthlib.common.generate_token') - def test_required_nonce(self, generate_token): - generate_token.return_value = 'abc' - self.request.nonce = None - self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) - - bearer = BearerToken(self.mock_validator) - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) From 4dd2b730759f9c3dedc6e77ccf79d779bbdad7f2 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:45:05 +0100 Subject: [PATCH 177/237] Added missing import after test moved --- tests/openid/connect/core/grant_types/test_hybrid.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/openid/connect/core/grant_types/test_hybrid.py b/tests/openid/connect/core/grant_types/test_hybrid.py index 8964053e..0aa0add6 100644 --- a/tests/openid/connect/core/grant_types/test_hybrid.py +++ b/tests/openid/connect/core/grant_types/test_hybrid.py @@ -1,13 +1,16 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals -from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant +import mock + +from oauthlib.oauth2.rfc6749 import errors +from oauthlib.oauth2.rfc6749.tokens import BearerToken +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant from tests.oauth2.rfc6749.grant_types.test_authorization_code import \ AuthorizationCodeGrantTest from .test_authorization_code import OpenIDAuthCodeTest - class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): """Test that OpenID don't interfere with normal OAuth 2 flows.""" From 8ef421b85b17b6cfb651ea4f13258be0c53ed879 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:57:53 +0100 Subject: [PATCH 178/237] Notifications must be sent for every build I hope fixing the longstanding issue mentionned at https://github.com/oauthlib/oauthlib/issues/582. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c7978d7c..f2e68a6a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ notifications: urls: - https://coveralls.io/webhook - https://webhooks.gitter.im/e/6008c872bf0ecee344f4 - on_success: change + on_success: always on_failure: always on_start: never deploy: From 73032fe688a899f80d2a65479c72fec450ec51a1 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 28 Feb 2019 10:06:37 +0100 Subject: [PATCH 179/237] Removed duplicated OIDC members in OAuth2.RequestValidator --- docs/oauth2/oidc/validator.rst | 6 +- oauthlib/oauth2/rfc6749/request_validator.py | 182 ------------------- oauthlib/openid/__init__.py | 1 + 3 files changed, 5 insertions(+), 184 deletions(-) diff --git a/docs/oauth2/oidc/validator.rst b/docs/oauth2/oidc/validator.rst index a03adfe4..7a6f5744 100644 --- a/docs/oauth2/oidc/validator.rst +++ b/docs/oauth2/oidc/validator.rst @@ -10,12 +10,14 @@ upgrade it by replacing one line of code: .. code-block:: python from oauthlib.oauth2 import Server + from oauthlib.oauth2 import RequestValidator Into .. code-block:: python from oauthlib.openid import Server + from oauthlib.openid import RequestValidator Then, you have to implement the new RequestValidator methods as shown below. @@ -24,5 +26,5 @@ RequestValidator Extension A couple of methods must be implemented in your validator subclass if you wish to support OpenID Connect: -.. autoclass:: oauthlib.oauth2.RequestValidator - :members: validate_silent_authorization, validate_silent_login, validate_user_match, get_id_token, get_authorization_code_scopes, validate_jwt_bearer_token +.. autoclass:: oauthlib.openid.RequestValidator + :members: diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 5ff30d80..d6ec2abf 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -291,32 +291,6 @@ def save_authorization_code(self, client_id, code, request, *args, **kwargs): """ raise NotImplementedError('Subclasses must implement this method.') - def get_authorization_code_scopes(self, client_id, code, redirect_uri, request): - """ Extracts scopes from saved authorization code. - - The scopes returned by this method is used to route token requests - based on scopes passed to Authorization Code requests. - - With that the token endpoint knows when to include OpenIDConnect - id_token in token response only based on authorization code scopes. - - Only code param should be sufficient to retrieve grant code from - any storage you are using, `client_id` and `redirect_uri` can gave a - blank value `""` don't forget to check it before using those values - in a select query if a database is used. - - :param client_id: Unicode client identifier. - :param code: Unicode authorization code grant. - :param redirect_uri: Unicode absolute URI. - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :return: A list of scopes - - Method is used by: - - Authorization Token Grant Dispatcher - """ - raise NotImplementedError('Subclasses must implement this method.') - def save_token(self, token, request, *args, **kwargs): """Persist the token with a token type specific method. @@ -378,104 +352,6 @@ def save_bearer_token(self, token, request, *args, **kwargs): """ raise NotImplementedError('Subclasses must implement this method.') - def get_jwt_bearer_token(self, token, token_handler, request): - """Get JWT Bearer token or OpenID Connect ID token - - If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token` - - :param token: A Bearer token dict. - :param token_handler: The token handler (BearerToken class). - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :return: The JWT Bearer token or OpenID Connect ID token (a JWS signed JWT) - - Method is used by JWT Bearer and OpenID Connect tokens: - - JWTToken.create_token - """ - raise NotImplementedError('Subclasses must implement this method.') - - def get_id_token(self, token, token_handler, request): - """Get OpenID Connect ID token - - In the OpenID Connect workflows when an ID Token is requested this method is called. - Subclasses should implement the construction, signing and optional encryption of the - ID Token as described in the OpenID Connect spec. - - In addition to the standard OAuth2 request properties, the request may also contain - these OIDC specific properties which are useful to this method: - - - nonce, if workflow is implicit or hybrid and it was provided - - claims, if provided to the original Authorization Code request - - The token parameter is a dict which may contain an ``access_token`` entry, in which - case the resulting ID Token *should* include a calculated ``at_hash`` claim. - - Similarly, when the request parameter has a ``code`` property defined, the ID Token - *should* include a calculated ``c_hash`` claim. - - http://openid.net/specs/openid-connect-core-1_0.html (sections `3.1.3.6`_, `3.2.2.10`_, `3.3.2.11`_) - - .. _`3.1.3.6`: http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken - .. _`3.2.2.10`: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken - .. _`3.3.2.11`: http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken - - :param token: A Bearer token dict. - :param token_handler: The token handler (BearerToken class) - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :return: The ID Token (a JWS signed JWT) - """ - # the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token - raise NotImplementedError('Subclasses must implement this method.') - - def validate_jwt_bearer_token(self, token, scopes, request): - """Ensure the JWT Bearer token or OpenID Connect ID token are valids and authorized access to scopes. - - If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token` - - If not using OpenID Connect this can `return None` to avoid 5xx rather 401/3 response. - - OpenID connect core 1.0 describe how to validate an id_token: - - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation - - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation - - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation - - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2 - - :param token: Unicode Bearer token. - :param scopes: List of scopes (defined by you). - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :rtype: True or False - - Method is indirectly used by all core OpenID connect JWT token issuing grant types: - - Authorization Code Grant - - Implicit Grant - - Hybrid Grant - """ - raise NotImplementedError('Subclasses must implement this method.') - - def validate_id_token(self, token, scopes, request): - """Ensure the id token is valid and authorized access to scopes. - - OpenID connect core 1.0 describe how to validate an id_token: - - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation - - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation - - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation - - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2 - - :param token: Unicode Bearer token. - :param scopes: List of scopes (defined by you). - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :rtype: True or False - - Method is indirectly used by all core OpenID connect JWT token issuing grant types: - - Authorization Code Grant - - Implicit Grant - - Hybrid Grant - """ - raise NotImplementedError('Subclasses must implement this method.') - def validate_bearer_token(self, token, scopes, request): """Ensure the Bearer token is valid and authorized access to scopes. @@ -668,44 +544,6 @@ def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): """ raise NotImplementedError('Subclasses must implement this method.') - def validate_silent_authorization(self, request): - """Ensure the logged in user has authorized silent OpenID authorization. - - Silent OpenID authorization allows access tokens and id tokens to be - granted to clients without any user prompt or interaction. - - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :rtype: True or False - - Method is used by: - - OpenIDConnectAuthCode - - OpenIDConnectImplicit - - OpenIDConnectHybrid - """ - raise NotImplementedError('Subclasses must implement this method.') - - def validate_silent_login(self, request): - """Ensure session user has authorized silent OpenID login. - - If no user is logged in or has not authorized silent login, this - method should return False. - - If the user is logged in but associated with multiple accounts and - not selected which one to link to the token then this method should - raise an oauthlib.oauth2.AccountSelectionRequired error. - - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :rtype: True or False - - Method is used by: - - OpenIDConnectAuthCode - - OpenIDConnectImplicit - - OpenIDConnectHybrid - """ - raise NotImplementedError('Subclasses must implement this method.') - def validate_user(self, username, password, client, request, *args, **kwargs): """Ensure the username and password is valid. @@ -726,26 +564,6 @@ def validate_user(self, username, password, client, request, *args, **kwargs): """ raise NotImplementedError('Subclasses must implement this method.') - def validate_user_match(self, id_token_hint, scopes, claims, request): - """Ensure client supplied user id hint matches session user. - - If the sub claim or id_token_hint is supplied then the session - user must match the given ID. - - :param id_token_hint: User identifier string. - :param scopes: List of OAuth 2 scopes and OpenID claims (strings). - :param claims: OpenID Connect claims dict. - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :rtype: True or False - - Method is used by: - - OpenIDConnectAuthCode - - OpenIDConnectImplicit - - OpenIDConnectHybrid - """ - raise NotImplementedError('Subclasses must implement this method.') - def is_pkce_required(self, client_id, request): """Determine if current request requires PKCE. Default, False. This is called for both "authorization" and "token" requests. diff --git a/oauthlib/openid/__init__.py b/oauthlib/openid/__init__.py index 03f0fa2e..7f1a8767 100644 --- a/oauthlib/openid/__init__.py +++ b/oauthlib/openid/__init__.py @@ -7,3 +7,4 @@ from __future__ import absolute_import, unicode_literals from .connect.core.endpoints import Server +from .connect.core.request_validator import RequestValidator From 7c570c763725fdaa40778d6cd6689b09b3971f50 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 28 Feb 2019 10:16:27 +0100 Subject: [PATCH 180/237] Add technicals fields of `id_token` in oauthlib OIDC support A new RequestValidator `fill_id_token` has been introduced to replace `get_id_token`. It aims to have the bare minimum amount of fields to complete a full OIDC id_token support. `get_id_token` is still valid but optional, and if it is implemented, `fill_id_token` will not be called. The current `fill_id_token` came with full support of `aud`, `iat`, `nonce`, `at_hash` and `c_hash`. More could come in the future e.g. `auth_time`, ... --- docs/oauth2/oidc/id_tokens.rst | 17 +++-- oauthlib/oauth2/rfc6749/request_validator.py | 3 + .../core/grant_types/authorization_code.py | 20 +++++ .../openid/connect/core/grant_types/base.py | 64 ++++++++++++++-- .../connect/core/grant_types/implicit.py | 4 +- .../openid/connect/core/request_validator.py | 75 ++++++++++++++++++- 6 files changed, 166 insertions(+), 17 deletions(-) diff --git a/docs/oauth2/oidc/id_tokens.rst b/docs/oauth2/oidc/id_tokens.rst index 999cfa7e..2387c01f 100644 --- a/docs/oauth2/oidc/id_tokens.rst +++ b/docs/oauth2/oidc/id_tokens.rst @@ -1,9 +1,9 @@ ID Tokens ========= -The creation of `ID Tokens`_ is ultimately done not by OAuthLib but by your ``RequestValidator`` subclass. This is because their +The creation of `ID Tokens`_ is ultimately not done by OAuthLib but by your ``RequestValidator`` subclass. This is because their content is dependent on your implementation of users, their attributes, any claims you may wish to support, as well as the -details of how you model the notion of a Client Application. As such OAuthLib simply calls your validator's ``get_id_token`` +details of how you model the notion of a Client Application. As such OAuthLib simply calls your validator's ``fill_id_token`` method at the appropriate times during the authorization flow, depending on the grant type requested (Authorization Code, Implicit, Hybrid, etc.). @@ -12,7 +12,7 @@ See examples below. .. _`ID Tokens`: http://openid.net/specs/openid-connect-core-1_0.html#IDToken .. autoclass:: oauthlib.oauth2.RequestValidator - :members: get_id_token + :members: fill_id_token JWT/JWS example with pyjwt library @@ -38,12 +38,13 @@ You can switch to jwcrypto library if you want to return JWE instead. super().__init__(self, **kwargs) - def get_id_token(self, token, token_handler, request): + def fill_id_token(self, id_token, token, token_handler, request): import jwt - data = {"nonce": request.nonce} if request.nonce is not None else {} - + id_token["iss"] = "https://my.cool.app.com" + id_token["sub"] = request.user.id + id_token["exp"] = id_token["iat"] + 3600 * 24 # keep it valid for 24hours for claim_key in request.claims: - data[claim_key] = request.userattributes[claim_key] # this must be set in another callback + id_token[claim_key] = request.userattributes[claim_key] # this must be set in another callback - return jwt.encode(data, self.private_pem, 'RS256') + return jwt.encode(id_token, self.private_pem, 'RS256') diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index d6ec2abf..86509b62 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -271,6 +271,9 @@ def save_authorization_code(self, client_id, code, request, *args, **kwargs): - Code Challenge (``request.code_challenge``) and - Code Challenge Method (``request.code_challenge_method``) + To support OIDC, you MUST associate the code with: + - nonce, if present (``code["nonce"]``) + The ``code`` argument is actually a dictionary, containing at least a ``code`` key with the actual authorization code: diff --git a/oauthlib/openid/connect/core/grant_types/authorization_code.py b/oauthlib/openid/connect/core/grant_types/authorization_code.py index b0b10155..becfcfab 100644 --- a/oauthlib/openid/connect/core/grant_types/authorization_code.py +++ b/oauthlib/openid/connect/core/grant_types/authorization_code.py @@ -22,3 +22,23 @@ def __init__(self, request_validator=None, **kwargs): self.custom_validators.post_auth.append( self.openid_authorization_validator) self.register_token_modifier(self.add_id_token) + + def add_id_token(self, token, token_handler, request): + """ + Construct an initial version of id_token, and let the + request_validator sign or encrypt it. + + The authorization_code version of this method is used to + retrieve the nonce accordingly to the code storage. + """ + # Treat it as normal OAuth 2 auth code request if openid is not present + if not request.scopes or 'openid' not in request.scopes: + return token + + nonce = self.request_validator.get_authorization_code_nonce( + request.client_id, + request.code, + request.redirect_uri, + request + ) + return super(AuthorizationCodeGrant, self).add_id_token(token, token_handler, request, nonce=nonce) diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index 4f5c9445..19a7f4f3 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -1,6 +1,8 @@ from .exceptions import OIDCNoPrompt +import base64 import datetime +import hashlib import logging from json import loads @@ -49,7 +51,45 @@ def _inflate_claims(self, request): raise InvalidRequestError(description="Malformed claims parameter", uri="http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter") - def add_id_token(self, token, token_handler, request): + def hash_id_token(self, value, hashfunc=hashlib.sha256): + """ + Its value is the base64url encoding of the left-most half of the + hash of the octets of the ASCII representation of the access_token + value, where the hash algorithm used is the hash algorithm used in + the alg Header Parameter of the ID Token's JOSE Header. + + For instance, if the alg is RS256, hash the access_token value + with SHA-256, then take the left-most 128 bits and + base64url-encode them. + For instance, if the alg is HS512, hash the code value with + SHA-512, then take the left-most 256 bits and base64url-encode + them. The c_hash value is a case-sensitive string. + + Example of hash from OIDC specification (bound to a JWS using RS256): + + code: + Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk + + c_hash: + LDktKdoQak3Pk0cnXxCltA + """ + digest = hashfunc(value.encode()).digest() + left_most = int(len(digest) / 2) + return base64.urlsafe_b64encode(digest[:left_most]).decode().rstrip("=") + + def add_id_token(self, token, token_handler, request, nonce=None): + """ + Construct an initial version of id_token, and let the + request_validator sign or encrypt it. + + The initial version can contain the fields below, accordingly + to the spec: + - aud + - iat + - nonce + - at_hash + - c_hash + """ # Treat it as normal OAuth 2 auth code request if openid is not present if not request.scopes or 'openid' not in request.scopes: return token @@ -58,13 +98,25 @@ def add_id_token(self, token, token_handler, request): if request.response_type and 'id_token' not in request.response_type: return token - if request.max_age: - d = datetime.datetime.utcnow() - token['auth_time'] = d.isoformat("T") + "Z" + # Implementation mint its own id_token without help. + id_token = self.request_validator.get_id_token(token, token_handler, request) + if id_token: + token['id_token'] = id_token + return token + + # Fallback for asking some help from oauthlib framework. + # Start with technicals fields bound to the specification. + id_token = {} + id_token['aud'] = request.client_id + id_token['iat'] = int(datetime.datetime.now().timestamp()) + if nonce is not None: + id_token["nonce"] = nonce - # TODO: acr claims (probably better handled by server code using oauthlib in get_id_token) + if "access_token" in token: + id_token["at_hash"] = self.hash_id_token(token["access_token"]) - token['id_token'] = self.request_validator.get_id_token(token, token_handler, request) + # Call request_validator to complete/sign/encrypt id_token + token['id_token'] = self.request_validator.fill_id_token(id_token, token, token_handler, request) return token diff --git a/oauthlib/openid/connect/core/grant_types/implicit.py b/oauthlib/openid/connect/core/grant_types/implicit.py index d3797b28..c2dbc278 100644 --- a/oauthlib/openid/connect/core/grant_types/implicit.py +++ b/oauthlib/openid/connect/core/grant_types/implicit.py @@ -27,9 +27,9 @@ def __init__(self, request_validator=None, **kwargs): self.register_token_modifier(self.add_id_token) def add_id_token(self, token, token_handler, request): - if 'state' not in token: + if 'state' not in token and request.state: token['state'] = request.state - return super(ImplicitGrant, self).add_id_token(token, token_handler, request) + return super(ImplicitGrant, self).add_id_token(token, token_handler, request, nonce=request.nonce) def openid_authorization_validator(self, request): """Additional validation when following the implicit flow. diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py index 15877541..f8aeed87 100644 --- a/oauthlib/openid/connect/core/request_validator.py +++ b/oauthlib/openid/connect/core/request_validator.py @@ -38,6 +38,31 @@ def get_authorization_code_scopes(self, client_id, code, redirect_uri, request): """ raise NotImplementedError('Subclasses must implement this method.') + def get_authorization_code_nonce(self, client_id, code, redirect_uri, request): + """ Extracts nonce from saved authorization code. + + If present in the Authentication Request, Authorization + Servers MUST include a nonce Claim in the ID Token with the + Claim Value being the nonce value sent in the Authentication + Request. Authorization Servers SHOULD perform no other + processing on nonce values used. The nonce value is a + case-sensitive string. + + Only code param should be sufficient to retrieve grant code from + any storage you are using, `client_id` and `redirect_uri` can gave a + blank value `""` don't forget to check it before using those values + in a select query if a database is used. + + :param client_id: Unicode client identifier + :param code: Unicode authorization code grant + :param redirect_uri: Unicode absolute URI + :return: A list of scope + + Method is used by: + - Authorization Token Grant Dispatcher + """ + raise NotImplementedError('Subclasses must implement this method.') + def get_jwt_bearer_token(self, token, token_handler, request): """Get JWT Bearer token or OpenID Connect ID token @@ -57,6 +82,12 @@ def get_jwt_bearer_token(self, token, token_handler, request): def get_id_token(self, token, token_handler, request): """Get OpenID Connect ID token + This method is OPTIONAL and is NOT RECOMMENDED. + `fill_id_token` SHOULD be implemented instead. However, if you + want a full control over the minting of the `id_token`, you + MAY want to override `get_id_token` instead of using + `fill_id_token`. + In the OpenID Connect workflows when an ID Token is requested this method is called. Subclasses should implement the construction, signing and optional encryption of the ID Token as described in the OpenID Connect spec. @@ -85,7 +116,49 @@ def get_id_token(self, token, token_handler, request): :type request: oauthlib.common.Request :return: The ID Token (a JWS signed JWT) """ - # the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token + return None + + def fill_id_token(self, id_token, token, token_handler, request): + """Fill OpenID Connect ID token & Sign or Encrypt. + + In the OpenID Connect workflows when an ID Token is requested + this method is called. Subclasses should implement the + construction, signing and optional encryption of the ID Token + as described in the OpenID Connect spec. + + The `id_token` parameter is a dict containing a couple of OIDC + technical fields related to the specification. Prepopulated + attributes are: + + - `aud`, equals to `request.client_id`. + - `iat`, equals to current time. + - `nonce`, if present, is equals to the `nonce` from the + authorization request. + - `at_hash`, hash of `access_token`, if relevant. + - `c_hash`, hash of `code`, if relevant. + + This method MUST provide required fields as below: + + - `iss`, REQUIRED. Issuer Identifier for the Issuer of the response. + - `sub`, REQUIRED. Subject Identifier + - `exp`, REQUIRED. Expiration time on or after which the ID + Token MUST NOT be accepted by the RP when performing + authentication with the OP. + + Additionals claims must be added, note that `request.scope` + should be used to determine the list of claims. + + More information can be found at `OpenID Connect Core#Claims`_ + + .. _`OpenID Connect Core#Claims`: https://openid.net/specs/openid-connect-core-1_0.html#Claims + + :param id_token: A dict containing technical fields of id_token + :param token: A Bearer token dict + :param token_handler: the token handler (BearerToken class) + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :return: The ID Token (a JWS signed JWT or JWE encrypted JWT) + """ raise NotImplementedError('Subclasses must implement this method.') def validate_jwt_bearer_token(self, token, scopes, request): From 62152d48e83cbc0eac3a2991b3b7fed2e84f7ec7 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 28 Feb 2019 15:03:34 +0100 Subject: [PATCH 181/237] Add c_hash. Add summary about when nonce/hashes are added to id_token --- .../openid/connect/core/grant_types/base.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index 19a7f4f3..f925c646 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -109,12 +109,41 @@ def add_id_token(self, token, token_handler, request, nonce=None): id_token = {} id_token['aud'] = request.client_id id_token['iat'] = int(datetime.datetime.now().timestamp()) + + # nonce is REQUIRED when response_type value is: + # - id_token token (Implicit) + # - id_token (Implicit) + # - code id_token (Hybrid) + # - code id_token token (Hybrid) + # + # nonce is OPTIONAL when response_type value is: + # - code (Authorization Code) + # - code token (Hybrid) if nonce is not None: id_token["nonce"] = nonce + # at_hash is REQUIRED when response_type value is: + # - id_token token (Implicit) + # - code id_token token (Hybrid) + # + # at_hash is OPTIONAL when: + # - code (Authorization code) + # - code id_token (Hybrid) + # - code token (Hybrid) + # + # at_hash MAY NOT be used when: + # - id_token (Implicit) if "access_token" in token: id_token["at_hash"] = self.hash_id_token(token["access_token"]) + # c_hash is REQUIRED when response_type value is: + # - code id_token (Hybrid) + # - code id_token token (Hybrid) + # + # c_hash is OPTIONAL for others. + if "code" in token: + id_token["c_hash"] = self.hash_id_token(token["code"]) + # Call request_validator to complete/sign/encrypt id_token token['id_token'] = self.request_validator.fill_id_token(id_token, token, token_handler, request) From 225399aad32d508a3cf00f8bcaaf7328e90cc904 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 28 Feb 2019 10:06:37 +0100 Subject: [PATCH 182/237] Removed duplicated OIDC members in OAuth2.RequestValidator --- docs/oauth2/oidc/validator.rst | 6 +- oauthlib/oauth2/rfc6749/request_validator.py | 182 ------------------- oauthlib/openid/__init__.py | 1 + 3 files changed, 5 insertions(+), 184 deletions(-) diff --git a/docs/oauth2/oidc/validator.rst b/docs/oauth2/oidc/validator.rst index a03adfe4..7a6f5744 100644 --- a/docs/oauth2/oidc/validator.rst +++ b/docs/oauth2/oidc/validator.rst @@ -10,12 +10,14 @@ upgrade it by replacing one line of code: .. code-block:: python from oauthlib.oauth2 import Server + from oauthlib.oauth2 import RequestValidator Into .. code-block:: python from oauthlib.openid import Server + from oauthlib.openid import RequestValidator Then, you have to implement the new RequestValidator methods as shown below. @@ -24,5 +26,5 @@ RequestValidator Extension A couple of methods must be implemented in your validator subclass if you wish to support OpenID Connect: -.. autoclass:: oauthlib.oauth2.RequestValidator - :members: validate_silent_authorization, validate_silent_login, validate_user_match, get_id_token, get_authorization_code_scopes, validate_jwt_bearer_token +.. autoclass:: oauthlib.openid.RequestValidator + :members: diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py index 5ff30d80..d6ec2abf 100644 --- a/oauthlib/oauth2/rfc6749/request_validator.py +++ b/oauthlib/oauth2/rfc6749/request_validator.py @@ -291,32 +291,6 @@ def save_authorization_code(self, client_id, code, request, *args, **kwargs): """ raise NotImplementedError('Subclasses must implement this method.') - def get_authorization_code_scopes(self, client_id, code, redirect_uri, request): - """ Extracts scopes from saved authorization code. - - The scopes returned by this method is used to route token requests - based on scopes passed to Authorization Code requests. - - With that the token endpoint knows when to include OpenIDConnect - id_token in token response only based on authorization code scopes. - - Only code param should be sufficient to retrieve grant code from - any storage you are using, `client_id` and `redirect_uri` can gave a - blank value `""` don't forget to check it before using those values - in a select query if a database is used. - - :param client_id: Unicode client identifier. - :param code: Unicode authorization code grant. - :param redirect_uri: Unicode absolute URI. - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :return: A list of scopes - - Method is used by: - - Authorization Token Grant Dispatcher - """ - raise NotImplementedError('Subclasses must implement this method.') - def save_token(self, token, request, *args, **kwargs): """Persist the token with a token type specific method. @@ -378,104 +352,6 @@ def save_bearer_token(self, token, request, *args, **kwargs): """ raise NotImplementedError('Subclasses must implement this method.') - def get_jwt_bearer_token(self, token, token_handler, request): - """Get JWT Bearer token or OpenID Connect ID token - - If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token` - - :param token: A Bearer token dict. - :param token_handler: The token handler (BearerToken class). - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :return: The JWT Bearer token or OpenID Connect ID token (a JWS signed JWT) - - Method is used by JWT Bearer and OpenID Connect tokens: - - JWTToken.create_token - """ - raise NotImplementedError('Subclasses must implement this method.') - - def get_id_token(self, token, token_handler, request): - """Get OpenID Connect ID token - - In the OpenID Connect workflows when an ID Token is requested this method is called. - Subclasses should implement the construction, signing and optional encryption of the - ID Token as described in the OpenID Connect spec. - - In addition to the standard OAuth2 request properties, the request may also contain - these OIDC specific properties which are useful to this method: - - - nonce, if workflow is implicit or hybrid and it was provided - - claims, if provided to the original Authorization Code request - - The token parameter is a dict which may contain an ``access_token`` entry, in which - case the resulting ID Token *should* include a calculated ``at_hash`` claim. - - Similarly, when the request parameter has a ``code`` property defined, the ID Token - *should* include a calculated ``c_hash`` claim. - - http://openid.net/specs/openid-connect-core-1_0.html (sections `3.1.3.6`_, `3.2.2.10`_, `3.3.2.11`_) - - .. _`3.1.3.6`: http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken - .. _`3.2.2.10`: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken - .. _`3.3.2.11`: http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken - - :param token: A Bearer token dict. - :param token_handler: The token handler (BearerToken class) - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :return: The ID Token (a JWS signed JWT) - """ - # the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token - raise NotImplementedError('Subclasses must implement this method.') - - def validate_jwt_bearer_token(self, token, scopes, request): - """Ensure the JWT Bearer token or OpenID Connect ID token are valids and authorized access to scopes. - - If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token` - - If not using OpenID Connect this can `return None` to avoid 5xx rather 401/3 response. - - OpenID connect core 1.0 describe how to validate an id_token: - - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation - - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation - - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation - - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2 - - :param token: Unicode Bearer token. - :param scopes: List of scopes (defined by you). - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :rtype: True or False - - Method is indirectly used by all core OpenID connect JWT token issuing grant types: - - Authorization Code Grant - - Implicit Grant - - Hybrid Grant - """ - raise NotImplementedError('Subclasses must implement this method.') - - def validate_id_token(self, token, scopes, request): - """Ensure the id token is valid and authorized access to scopes. - - OpenID connect core 1.0 describe how to validate an id_token: - - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation - - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation - - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation - - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2 - - :param token: Unicode Bearer token. - :param scopes: List of scopes (defined by you). - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :rtype: True or False - - Method is indirectly used by all core OpenID connect JWT token issuing grant types: - - Authorization Code Grant - - Implicit Grant - - Hybrid Grant - """ - raise NotImplementedError('Subclasses must implement this method.') - def validate_bearer_token(self, token, scopes, request): """Ensure the Bearer token is valid and authorized access to scopes. @@ -668,44 +544,6 @@ def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): """ raise NotImplementedError('Subclasses must implement this method.') - def validate_silent_authorization(self, request): - """Ensure the logged in user has authorized silent OpenID authorization. - - Silent OpenID authorization allows access tokens and id tokens to be - granted to clients without any user prompt or interaction. - - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :rtype: True or False - - Method is used by: - - OpenIDConnectAuthCode - - OpenIDConnectImplicit - - OpenIDConnectHybrid - """ - raise NotImplementedError('Subclasses must implement this method.') - - def validate_silent_login(self, request): - """Ensure session user has authorized silent OpenID login. - - If no user is logged in or has not authorized silent login, this - method should return False. - - If the user is logged in but associated with multiple accounts and - not selected which one to link to the token then this method should - raise an oauthlib.oauth2.AccountSelectionRequired error. - - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :rtype: True or False - - Method is used by: - - OpenIDConnectAuthCode - - OpenIDConnectImplicit - - OpenIDConnectHybrid - """ - raise NotImplementedError('Subclasses must implement this method.') - def validate_user(self, username, password, client, request, *args, **kwargs): """Ensure the username and password is valid. @@ -726,26 +564,6 @@ def validate_user(self, username, password, client, request, *args, **kwargs): """ raise NotImplementedError('Subclasses must implement this method.') - def validate_user_match(self, id_token_hint, scopes, claims, request): - """Ensure client supplied user id hint matches session user. - - If the sub claim or id_token_hint is supplied then the session - user must match the given ID. - - :param id_token_hint: User identifier string. - :param scopes: List of OAuth 2 scopes and OpenID claims (strings). - :param claims: OpenID Connect claims dict. - :param request: OAuthlib request. - :type request: oauthlib.common.Request - :rtype: True or False - - Method is used by: - - OpenIDConnectAuthCode - - OpenIDConnectImplicit - - OpenIDConnectHybrid - """ - raise NotImplementedError('Subclasses must implement this method.') - def is_pkce_required(self, client_id, request): """Determine if current request requires PKCE. Default, False. This is called for both "authorization" and "token" requests. diff --git a/oauthlib/openid/__init__.py b/oauthlib/openid/__init__.py index 03f0fa2e..7f1a8767 100644 --- a/oauthlib/openid/__init__.py +++ b/oauthlib/openid/__init__.py @@ -7,3 +7,4 @@ from __future__ import absolute_import, unicode_literals from .connect.core.endpoints import Server +from .connect.core.request_validator import RequestValidator From 7f7d38e6a48b85b906fc14ce186fa39ee468176d Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 28 Feb 2019 15:26:09 +0100 Subject: [PATCH 183/237] Fixed missing references in unittests --- tests/openid/connect/core/endpoints/test_claims_handling.py | 2 +- tests/openid/connect/core/test_request_validator.py | 2 +- tests/openid/connect/core/test_tokens.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/openid/connect/core/endpoints/test_claims_handling.py b/tests/openid/connect/core/endpoints/test_claims_handling.py index 270ef69c..5f39d965 100644 --- a/tests/openid/connect/core/endpoints/test_claims_handling.py +++ b/tests/openid/connect/core/endpoints/test_claims_handling.py @@ -10,7 +10,7 @@ import mock -from oauthlib.oauth2 import RequestValidator +from oauthlib.openid import RequestValidator from oauthlib.openid.connect.core.endpoints.pre_configured import Server from tests.unittest import TestCase diff --git a/tests/openid/connect/core/test_request_validator.py b/tests/openid/connect/core/test_request_validator.py index 1e71fb16..e20e88f0 100644 --- a/tests/openid/connect/core/test_request_validator.py +++ b/tests/openid/connect/core/test_request_validator.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals -from oauthlib.openid.connect.core.request_validator import RequestValidator +from oauthlib.openid import RequestValidator from tests.unittest import TestCase diff --git a/tests/openid/connect/core/test_tokens.py b/tests/openid/connect/core/test_tokens.py index 1fcfb517..fde89d62 100644 --- a/tests/openid/connect/core/test_tokens.py +++ b/tests/openid/connect/core/test_tokens.py @@ -42,7 +42,7 @@ def test_create_token_calls_get_id_token(self): """ request_mock = mock.MagicMock() - with mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + with mock.patch('oauthlib.openid.RequestValidator', autospec=True) as RequestValidatorMock: request_validator = RequestValidatorMock() @@ -58,7 +58,7 @@ def test_validate_request_token_from_headers(self): """ with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \ - mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + mock.patch('oauthlib.openid.RequestValidator', autospec=True) as RequestValidatorMock: request_validator_mock = RequestValidatorMock() @@ -84,7 +84,7 @@ def test_validate_token_from_request(self): """ with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \ - mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator', + mock.patch('oauthlib.openid.RequestValidator', autospec=True) as RequestValidatorMock: request_validator_mock = RequestValidatorMock() From 0a1b4f34d1d2f3fcbca8a7ff2d07b78612a3b766 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 2 Mar 2019 09:01:43 -0800 Subject: [PATCH 184/237] Combine multiple isinstance() calls to one --- oauthlib/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/common.py b/oauthlib/common.py index 970d7a5b..96de1f17 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -172,7 +172,7 @@ def extract_params(raw): empty list of parameters. Any other input will result in a return value of None. """ - if isinstance(raw, bytes) or isinstance(raw, unicode_type): + if isinstance(raw, (bytes, unicode_type)): try: params = urldecode(raw) except ValueError: From 84cd5a4265c2670af5a4b7ad2143c47fa68582c1 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 15:36:13 +0100 Subject: [PATCH 185/237] Change to 3.0.2-dev as long as master is in "dev" --- CHANGELOG.rst | 2 +- oauthlib/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f49fb928..ade6243b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -TBD +3.0.2 (TBD) ------------------ * #650 Fixed space encoding in base string URI used in the signature base string. * #652: Fixed OIDC /token response which wrongly returned "&state=None" diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index b23102c3..8eb82a65 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,6 +12,6 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.0.1' +__version__ = '3.0.2-dev' logging.getLogger('oauthlib').addHandler(NullHandler()) From 53d3d335879f205ae705d93420f34984073cd5a1 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 26 Mar 2019 14:50:41 +0100 Subject: [PATCH 186/237] Renamed fill into finalize to add clarity --- docs/oauth2/oidc/id_tokens.rst | 6 +++--- oauthlib/openid/connect/core/grant_types/base.py | 2 +- oauthlib/openid/connect/core/request_validator.py | 8 ++++---- tests/openid/connect/core/test_request_validator.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/oauth2/oidc/id_tokens.rst b/docs/oauth2/oidc/id_tokens.rst index 2387c01f..a1bf7cf3 100644 --- a/docs/oauth2/oidc/id_tokens.rst +++ b/docs/oauth2/oidc/id_tokens.rst @@ -3,7 +3,7 @@ ID Tokens The creation of `ID Tokens`_ is ultimately not done by OAuthLib but by your ``RequestValidator`` subclass. This is because their content is dependent on your implementation of users, their attributes, any claims you may wish to support, as well as the -details of how you model the notion of a Client Application. As such OAuthLib simply calls your validator's ``fill_id_token`` +details of how you model the notion of a Client Application. As such OAuthLib simply calls your validator's ``finalize_id_token`` method at the appropriate times during the authorization flow, depending on the grant type requested (Authorization Code, Implicit, Hybrid, etc.). @@ -12,7 +12,7 @@ See examples below. .. _`ID Tokens`: http://openid.net/specs/openid-connect-core-1_0.html#IDToken .. autoclass:: oauthlib.oauth2.RequestValidator - :members: fill_id_token + :members: finalize_id_token JWT/JWS example with pyjwt library @@ -38,7 +38,7 @@ You can switch to jwcrypto library if you want to return JWE instead. super().__init__(self, **kwargs) - def fill_id_token(self, id_token, token, token_handler, request): + def finalize_id_token(self, id_token, token, token_handler, request): import jwt id_token["iss"] = "https://my.cool.app.com" diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index f925c646..31ff82ef 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -145,7 +145,7 @@ def add_id_token(self, token, token_handler, request, nonce=None): id_token["c_hash"] = self.hash_id_token(token["code"]) # Call request_validator to complete/sign/encrypt id_token - token['id_token'] = self.request_validator.fill_id_token(id_token, token, token_handler, request) + token['id_token'] = self.request_validator.finalize_id_token(id_token, token, token_handler, request) return token diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py index f8aeed87..7ce7e170 100644 --- a/oauthlib/openid/connect/core/request_validator.py +++ b/oauthlib/openid/connect/core/request_validator.py @@ -83,10 +83,10 @@ def get_id_token(self, token, token_handler, request): """Get OpenID Connect ID token This method is OPTIONAL and is NOT RECOMMENDED. - `fill_id_token` SHOULD be implemented instead. However, if you + `finalize_id_token` SHOULD be implemented instead. However, if you want a full control over the minting of the `id_token`, you MAY want to override `get_id_token` instead of using - `fill_id_token`. + `finalize_id_token`. In the OpenID Connect workflows when an ID Token is requested this method is called. Subclasses should implement the construction, signing and optional encryption of the @@ -118,8 +118,8 @@ def get_id_token(self, token, token_handler, request): """ return None - def fill_id_token(self, id_token, token, token_handler, request): - """Fill OpenID Connect ID token & Sign or Encrypt. + def finalize_id_token(self, id_token, token, token_handler, request): + """Finalize OpenID Connect ID token & Sign or Encrypt. In the OpenID Connect workflows when an ID Token is requested this method is called. Subclasses should implement the diff --git a/tests/openid/connect/core/test_request_validator.py b/tests/openid/connect/core/test_request_validator.py index e20e88f0..ebe0aeb7 100644 --- a/tests/openid/connect/core/test_request_validator.py +++ b/tests/openid/connect/core/test_request_validator.py @@ -22,8 +22,8 @@ def test_method_contracts(self): ) self.assertRaises( NotImplementedError, - v.get_id_token, - 'token', 'token_handler', 'request' + v.finalize_id_token, + 'id_token', 'token', 'token_handler', 'request' ) self.assertRaises( NotImplementedError, From 4877b4837a9355bc74c9f3d59343d689be4c86fa Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 26 Mar 2019 14:50:55 +0100 Subject: [PATCH 187/237] Use native operator instead type conversion --- oauthlib/openid/connect/core/grant_types/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index 31ff82ef..c5d91e72 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -74,7 +74,7 @@ def hash_id_token(self, value, hashfunc=hashlib.sha256): LDktKdoQak3Pk0cnXxCltA """ digest = hashfunc(value.encode()).digest() - left_most = int(len(digest) / 2) + left_most = len(digest) // 2 return base64.urlsafe_b64encode(digest[:left_most]).decode().rstrip("=") def add_id_token(self, token, token_handler, request, nonce=None): From 09538c93d562f6230f3d257b6782d58eeb0a7c3e Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 26 Mar 2019 16:15:13 +0100 Subject: [PATCH 188/237] Add unittests for OIDC GrantTypeBase. Rename hash_id_token into id_token_hash --- .../openid/connect/core/grant_types/base.py | 6 +- .../connect/core/grant_types/test_base.py | 104 ++++++++++++++++++ 2 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 tests/openid/connect/core/grant_types/test_base.py diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index c5d91e72..6272ea27 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -51,7 +51,7 @@ def _inflate_claims(self, request): raise InvalidRequestError(description="Malformed claims parameter", uri="http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter") - def hash_id_token(self, value, hashfunc=hashlib.sha256): + def id_token_hash(self, value, hashfunc=hashlib.sha256): """ Its value is the base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the access_token @@ -134,7 +134,7 @@ def add_id_token(self, token, token_handler, request, nonce=None): # at_hash MAY NOT be used when: # - id_token (Implicit) if "access_token" in token: - id_token["at_hash"] = self.hash_id_token(token["access_token"]) + id_token["at_hash"] = self.id_token_hash(token["access_token"]) # c_hash is REQUIRED when response_type value is: # - code id_token (Hybrid) @@ -142,7 +142,7 @@ def add_id_token(self, token, token_handler, request, nonce=None): # # c_hash is OPTIONAL for others. if "code" in token: - id_token["c_hash"] = self.hash_id_token(token["code"]) + id_token["c_hash"] = self.id_token_hash(token["code"]) # Call request_validator to complete/sign/encrypt id_token token['id_token'] = self.request_validator.finalize_id_token(id_token, token, token_handler, request) diff --git a/tests/openid/connect/core/grant_types/test_base.py b/tests/openid/connect/core/grant_types/test_base.py new file mode 100644 index 00000000..319904b8 --- /dev/null +++ b/tests/openid/connect/core/grant_types/test_base.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +import datetime +import mock + +from oauthlib.common import Request +from oauthlib.openid.connect.core.grant_types.base import GrantTypeBase + +from tests.unittest import TestCase + + +class GrantBase(GrantTypeBase): + """Class to test GrantTypeBase""" + def __init__(self, request_validator=None, **kwargs): + self.request_validator = request_validator + + +class IDTokenTest(TestCase): + + def setUp(self): + self.request = Request('http://a.b/path') + self.request.scopes = ('hello', 'openid') + self.request.expires_in = 1800 + self.request.client_id = 'abcdef' + self.request.code = '1234' + self.request.response_type = 'id_token' + self.request.grant_type = 'authorization_code' + self.request.redirect_uri = 'https://a.b/cb' + self.request.state = 'abc' + self.request.nonce = None + + self.mock_validator = mock.MagicMock() + self.mock_validator.get_id_token.return_value = None + self.mock_validator.finalize_id_token.return_value = "eyJ.body.signature" + self.token = {} + + self.grant = GrantBase(request_validator=self.mock_validator) + + self.url_query = 'https://a.b/cb?code=abc&state=abc' + self.url_fragment = 'https://a.b/cb#code=abc&state=abc' + + def test_id_token_hash(self): + self.assertEqual(self.grant.id_token_hash( + "Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk", + ), "LDktKdoQak3Pk0cnXxCltA", "hash differs from RFC") + + def test_get_id_token_no_openid(self): + self.request.scopes = ('hello') + token = self.grant.add_id_token(self.token, "token_handler_mock", self.request) + self.assertNotIn("id_token", token) + + self.request.scopes = None + token = self.grant.add_id_token(self.token, "token_handler_mock", self.request) + self.assertNotIn("id_token", token) + + self.request.scopes = () + token = self.grant.add_id_token(self.token, "token_handler_mock", self.request) + self.assertNotIn("id_token", token) + + def test_get_id_token(self): + self.mock_validator.get_id_token.return_value = "toto" + token = self.grant.add_id_token(self.token, "token_handler_mock", self.request) + self.assertIn("id_token", token) + self.assertEqual(token["id_token"], "toto") + + def test_finalize_id_token(self): + token = self.grant.add_id_token(self.token, "token_handler_mock", self.request) + self.assertIn("id_token", token) + 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(datetime.datetime.now().timestamp())) + + def test_finalize_id_token_with_nonce(self): + token = self.grant.add_id_token(self.token, "token_handler_mock", self.request, "my_nonce") + self.assertIn("id_token", token) + self.assertEqual(token["id_token"], "eyJ.body.signature") + id_token = self.mock_validator.finalize_id_token.call_args[0][0] + self.assertEqual(id_token['nonce'], 'my_nonce') + + def test_finalize_id_token_with_at_hash(self): + self.token["access_token"] = "Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk" + token = self.grant.add_id_token(self.token, "token_handler_mock", self.request) + self.assertIn("id_token", token) + self.assertEqual(token["id_token"], "eyJ.body.signature") + id_token = self.mock_validator.finalize_id_token.call_args[0][0] + self.assertEqual(id_token['at_hash'], 'LDktKdoQak3Pk0cnXxCltA') + + def test_finalize_id_token_with_c_hash(self): + self.token["code"] = "Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk" + token = self.grant.add_id_token(self.token, "token_handler_mock", self.request) + self.assertIn("id_token", token) + self.assertEqual(token["id_token"], "eyJ.body.signature") + id_token = self.mock_validator.finalize_id_token.call_args[0][0] + self.assertEqual(id_token['c_hash'], 'LDktKdoQak3Pk0cnXxCltA') + + def test_finalize_id_token_with_c_and_at_hash(self): + self.token["code"] = "Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk" + self.token["access_token"] = "Qcb0Orv1zh30vL1MPRsbm-diHiMwcLyZvn1arpZv-Jxf_11jnpEX3Tgfvk" + token = self.grant.add_id_token(self.token, "token_handler_mock", self.request) + self.assertIn("id_token", token) + self.assertEqual(token["id_token"], "eyJ.body.signature") + id_token = self.mock_validator.finalize_id_token.call_args[0][0] + self.assertEqual(id_token['at_hash'], 'LDktKdoQak3Pk0cnXxCltA') + self.assertEqual(id_token['c_hash'], 'LDktKdoQak3Pk0cnXxCltA') From ed8c4f253def93a0d4d78a6ead1a63091f8e4c26 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 26 Mar 2019 16:32:51 +0100 Subject: [PATCH 189/237] Python2.7 compatible --- oauthlib/openid/connect/core/grant_types/base.py | 4 ++-- tests/openid/connect/core/grant_types/test_base.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index 6272ea27..32a21b62 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -1,9 +1,9 @@ from .exceptions import OIDCNoPrompt import base64 -import datetime import hashlib import logging +import time from json import loads from oauthlib.oauth2.rfc6749.errors import ConsentRequired, InvalidRequestError, LoginRequired @@ -108,7 +108,7 @@ def add_id_token(self, token, token_handler, request, nonce=None): # Start with technicals fields bound to the specification. id_token = {} id_token['aud'] = request.client_id - id_token['iat'] = int(datetime.datetime.now().timestamp()) + id_token['iat'] = int(time.time()) # nonce is REQUIRED when response_type value is: # - id_token token (Implicit) diff --git a/tests/openid/connect/core/grant_types/test_base.py b/tests/openid/connect/core/grant_types/test_base.py index 319904b8..76e017f9 100644 --- a/tests/openid/connect/core/grant_types/test_base.py +++ b/tests/openid/connect/core/grant_types/test_base.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -import datetime import mock +import time from oauthlib.common import Request from oauthlib.openid.connect.core.grant_types.base import GrantTypeBase @@ -68,7 +68,7 @@ def test_finalize_id_token(self): 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(datetime.datetime.now().timestamp())) + self.assertGreaterEqual(id_token['iat'], int(time.time())) def test_finalize_id_token_with_nonce(self): token = self.grant.add_id_token(self.token, "token_handler_mock", self.request, "my_nonce") From 43eece8bddccb1453481b5962c3cff279c7b4ea8 Mon Sep 17 00:00:00 2001 From: Arjan Keeman Date: Thu, 21 Mar 2019 14:17:58 +0100 Subject: [PATCH 190/237] fix include_client_id argument --- .../rfc6749/clients/backend_application.py | 16 ++++----- .../rfc6749/clients/legacy_application.py | 14 ++++---- .../rfc6749/clients/service_application.py | 36 +++++++++---------- oauthlib/oauth2/rfc6749/parameters.py | 4 +-- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py index a000ecf5..2483e56e 100644 --- a/oauthlib/oauth2/rfc6749/clients/backend_application.py +++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py @@ -29,11 +29,11 @@ class BackendApplicationClient(Client): Since the client authentication is used as the authorization grant, no additional authorization request is needed. """ - + grant_type = 'client_credentials' - + def prepare_request_body(self, body='', scope=None, - include_client_id=None, **kwargs): + include_client_id=False, **kwargs): """Add the client credentials to the request body. The client makes a request to the token endpoint by adding the @@ -45,11 +45,11 @@ def prepare_request_body(self, body='', scope=None, :param scope: The scope of the access request as described by `Section 3.3`_. - :param include_client_id: `True` to send the `client_id` in the body of - the upstream request. Default `None`. This is - required if the client is not authenticating - with the authorization server as described - in `Section 3.2.1`_. + :param include_client_id: `True` to send the `client_id` in the + body of the upstream request. This is required + if the client is not authenticating with the + authorization server as described in + `Section 3.2.1`_. False otherwise (default). :type include_client_id: Boolean :param kwargs: Extra credentials to include in the token request. diff --git a/oauthlib/oauth2/rfc6749/clients/legacy_application.py b/oauthlib/oauth2/rfc6749/clients/legacy_application.py index 2449363a..ca218e45 100644 --- a/oauthlib/oauth2/rfc6749/clients/legacy_application.py +++ b/oauthlib/oauth2/rfc6749/clients/legacy_application.py @@ -34,14 +34,14 @@ class LegacyApplicationClient(Client): credentials is beyond the scope of this specification. The client MUST discard the credentials once an access token has been obtained. """ - + grant_type = 'password' def __init__(self, client_id, **kwargs): super(LegacyApplicationClient, self).__init__(client_id, **kwargs) def prepare_request_body(self, username, password, body='', scope=None, - include_client_id=None, **kwargs): + include_client_id=False, **kwargs): """Add the resource owner password and username to the request body. The client makes a request to the token endpoint by adding the @@ -54,11 +54,11 @@ def prepare_request_body(self, username, password, body='', scope=None, into. This may contain extra paramters. Default ''. :param scope: The scope of the access request as described by `Section 3.3`_. - :param include_client_id: `True` to send the `client_id` in the body of - the upstream request. Default `None`. This is - required if the client is not authenticating - with the authorization server as described - in `Section 3.2.1`_. + :param include_client_id: `True` to send the `client_id` in the + body of the upstream request. This is required + if the client is not authenticating with the + authorization server as described in + `Section 3.2.1`_. False otherwise (default). :type include_client_id: Boolean :param kwargs: Extra credentials to include in the token request. diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index 35333d81..ea946cec 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -41,20 +41,20 @@ def __init__(self, client_id, private_key=None, subject=None, issuer=None, :param private_key: Private key used for signing and encrypting. Must be given as a string. - :param subject: The principal that is the subject of the JWT, i.e. + :param subject: The principal that is the subject of the JWT, i.e. which user is the token requested on behalf of. For example, ``foo@example.com. :param issuer: The JWT MUST contain an "iss" (issuer) claim that contains a unique identifier for the entity that issued - the JWT. For example, ``your-client@provider.com``. + the JWT. For example, ``your-client@provider.com``. :param audience: A value identifying the authorization server as an intended audience, e.g. ``https://provider.com/oauth2/token``. :param kwargs: Additional arguments to pass to base client, such as - state and token. See ``Client.__init__.__doc__`` for + state and token. See ``Client.__init__.__doc__`` for details. """ super(ServiceApplicationClient, self).__init__(client_id, **kwargs) @@ -63,17 +63,17 @@ def __init__(self, client_id, private_key=None, subject=None, issuer=None, self.issuer = issuer self.audience = audience - def prepare_request_body(self, + def prepare_request_body(self, private_key=None, - subject=None, - issuer=None, - audience=None, - expires_at=None, + subject=None, + issuer=None, + audience=None, + expires_at=None, issued_at=None, extra_claims=None, - body='', + body='', scope=None, - include_client_id=None, + include_client_id=False, **kwargs): """Create and add a JWT assertion to the request body. @@ -86,7 +86,7 @@ def prepare_request_body(self, :param issuer: (iss) The JWT MUST contain an "iss" (issuer) claim that contains a unique identifier for the entity that issued - the JWT. For example, ``your-client@provider.com``. + the JWT. For example, ``your-client@provider.com``. :param audience: (aud) A value identifying the authorization server as an intended audience, e.g. @@ -105,11 +105,11 @@ def prepare_request_body(self, :param scope: The scope of the access request. - :param include_client_id: `True` to send the `client_id` in the body of - the upstream request. Default `None`. This is - required if the client is not authenticating - with the authorization server as described - in `Section 3.2.1`_. + :param include_client_id: `True` to send the `client_id` in the + body of the upstream request. This is required + if the client is not authenticating with the + authorization server as described in + `Section 3.2.1`_. False otherwise (default). :type include_client_id: Boolean :param not_before: A unix timestamp after which the JWT may be used. @@ -129,7 +129,7 @@ def prepare_request_body(self, [I-D.ietf-oauth-assertions] specification, to indicate the requested scope. - Authentication of the client is optional, as described in + Authentication of the client is optional, as described in `Section 3.2.1`_ of OAuth 2.0 [RFC6749] and consequently, the "client_id" is only needed when a form of client authentication that relies on the parameter is used. @@ -186,5 +186,5 @@ def prepare_request_body(self, return prepare_token_request(self.grant_type, body=body, assertion=assertion, - scope=scope, + scope=scope, **kwargs) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 4d0baee7..6b9d6306 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -98,7 +98,7 @@ def prepare_token_request(grant_type, body='', include_client_id=True, **kwargs) "authorization_code" or "client_credentials". :param body: Existing request body (URL encoded string) to embed parameters - into. This may contain extra paramters. Default ''. + into. This may contain extra parameters. Default ''. :param include_client_id: `True` (default) to send the `client_id` in the body of the upstream request. This is required @@ -142,7 +142,7 @@ def prepare_token_request(grant_type, body='', include_client_id=True, **kwargs) if 'scope' in kwargs: kwargs['scope'] = list_to_scope(kwargs['scope']) - # pull the `client_id` out of the kwargs. + # pull the `client_id` out of the kwargs. client_id = kwargs.pop('client_id', None) if include_client_id: if client_id is not None: From 5ae97b984f9fe29717c125c205f79c87c6370613 Mon Sep 17 00:00:00 2001 From: Abhishek Patel Date: Sun, 21 Apr 2019 12:09:00 -0700 Subject: [PATCH 191/237] Add method to get/set debug flag - By default debug mode is always off - Debug mode turned on automatically for tests - Complete requests sanitized in non debug mode --- oauthlib/__init__.py | 9 +++++++++ oauthlib/common.py | 3 +++ tests/__init__.py | 3 +++ tests/test_common.py | 16 ++++++++++++++++ 4 files changed, 31 insertions(+) diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 8eb82a65..8639e88a 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -15,3 +15,12 @@ __version__ = '3.0.2-dev' logging.getLogger('oauthlib').addHandler(NullHandler()) + +_DEBUG = False + +def set_debug(debug_val): + global _DEBUG + _DEBUG = debug_val + +def get_debug_flag(): + return _DEBUG diff --git a/oauthlib/common.py b/oauthlib/common.py index 96de1f17..ea5bfe79 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -14,6 +14,7 @@ import re import sys import time +from . import get_debug_flag try: from secrets import randbits @@ -435,6 +436,8 @@ def __getattr__(self, name): raise AttributeError(name) def __repr__(self): + if not get_debug_flag(): + return "" body = self.body headers = self.headers.copy() if body: diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..f33236b5 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +import oauthlib + +oauthlib.set_debug(True) diff --git a/tests/test_common.py b/tests/test_common.py index 20d9f5b7..ae2531b2 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals +import os import sys +import oauthlib from oauthlib.common import (CaseInsensitiveDict, Request, add_params_to_uri, extract_params, generate_client_id, generate_nonce, generate_timestamp, @@ -214,6 +216,20 @@ def test_headers_params(self): self.assertEqual(r.headers['token'], 'foobar') self.assertEqual(r.token, 'banana') + def test_sanitized_request_non_debug_mode(self): + """make sure requests are sanitized when in non debug mode. + For the debug mode, the other tests checking sanitization should prove + that debug mode is working. + """ + try: + oauthlib.set_debug(False) + r = Request(URI, headers={'token': 'foobar'}, body='token=banana') + self.assertNotIn('token', repr(r)) + self.assertIn('SANITIZED', repr(r)) + finally: + # set flag back for other tests + oauthlib.set_debug(True) + class CaseInsensitiveDictTest(TestCase): From 53b2db53a9b91183e8828f138ca3d64dd601584c Mon Sep 17 00:00:00 2001 From: Abhishek Patel Date: Sun, 21 Apr 2019 12:13:06 -0700 Subject: [PATCH 192/237] add doc --- oauthlib/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 8639e88a..25132f08 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -19,8 +19,16 @@ _DEBUG = False def set_debug(debug_val): + """Set value of debug flag + + :param debug_val: Value to set. Must be a bool value. + """ global _DEBUG _DEBUG = debug_val def get_debug_flag(): + """Get debug mode value. + + :return: `True` if debug mode is on, `False` otherwise + """ return _DEBUG From 8390fa8ec6c019b3403b05287392e7a517c71963 Mon Sep 17 00:00:00 2001 From: Abhishek Patel Date: Tue, 23 Apr 2019 19:39:50 -0700 Subject: [PATCH 193/237] refactor to get_debug - Oauthlib's debug mode can be checked with method --- oauthlib/__init__.py | 2 +- oauthlib/common.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 25132f08..0e413bc3 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -26,7 +26,7 @@ def set_debug(debug_val): global _DEBUG _DEBUG = debug_val -def get_debug_flag(): +def get_debug(): """Get debug mode value. :return: `True` if debug mode is on, `False` otherwise diff --git a/oauthlib/common.py b/oauthlib/common.py index ea5bfe79..5aeb0150 100644 --- a/oauthlib/common.py +++ b/oauthlib/common.py @@ -14,7 +14,7 @@ import re import sys import time -from . import get_debug_flag +from . import get_debug try: from secrets import randbits @@ -436,7 +436,7 @@ def __getattr__(self, name): raise AttributeError(name) def __repr__(self): - if not get_debug_flag(): + if not get_debug(): return "" body = self.body headers = self.headers.copy() From 924a58fa1a9cc6eb2175ac038ca409708951915b Mon Sep 17 00:00:00 2001 From: Abhishek Patel Date: Tue, 23 Apr 2019 21:08:04 -0700 Subject: [PATCH 194/237] Update documentation --- docs/error_reporting.rst | 6 +++++- docs/oauth1/server.rst | 2 ++ docs/oauth2/server.rst | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/error_reporting.rst b/docs/error_reporting.rst index 705f4478..a80287bf 100644 --- a/docs/error_reporting.rst +++ b/docs/error_reporting.rst @@ -10,16 +10,20 @@ case where that is not true please let us know! When reporting bugs, especially when they are hard or impossible to reproduce, it is useful to include logging output. You can enable logging for all -oauthlib modules by adding a logger to the `oauthlib` namespace. +oauthlib modules by adding a logger to the `oauthlib` namespace. You might also +want to enable debugging mode to include request data in output. .. code-block:: python import logging + import oauthlib import sys + oauthlib.set_debug(True) log = logging.getLogger('oauthlib') log.addHandler(logging.StreamHandler(sys.stdout)) log.setLevel(logging.DEBUG) + If you are using a library that builds upon OAuthLib please also enable the logging for their modules, e.g. for `requests-oauthlib` diff --git a/docs/oauth1/server.rst b/docs/oauth1/server.rst index db469d2c..2f30c65b 100644 --- a/docs/oauth1/server.rst +++ b/docs/oauth1/server.rst @@ -441,7 +441,9 @@ Drop a line in our `Gitter OAuthLib community`_ or open a `GitHub issue`_ =) If you run into issues it can be helpful to enable debug logging:: import logging + import oauthlib import sys + oauthlib.set_debug(True) log = logging.getLogger('oauthlib') log.addHandler(logging.StreamHandler(sys.stdout)) log.setLevel(logging.DEBUG) diff --git a/docs/oauth2/server.rst b/docs/oauth2/server.rst index dad0aae1..d9846c53 100644 --- a/docs/oauth2/server.rst +++ b/docs/oauth2/server.rst @@ -524,7 +524,10 @@ If you run into issues it can be helpful to enable debug logging. .. code-block:: python import logging + import oauthlib import sys + + oauthlib.set_debug(True) log = logging.getLogger('oauthlib') log.addHandler(logging.StreamHandler(sys.stdout)) log.setLevel(logging.DEBUG) From 71be50afdeaf99a0ba6ce5048851dcdd5620d880 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 26 Apr 2019 15:59:57 +0200 Subject: [PATCH 195/237] Fix 670. AuthCode API must return the new PKCE attribute --- oauthlib/oauth2/rfc6749/grant_types/authorization_code.py | 3 +++ tests/oauth2/rfc6749/grant_types/test_authorization_code.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 5f03d9cb..9b84c4c4 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -405,12 +405,15 @@ def validate_authorization_request(self, request): raise errors.MissingCodeChallengeError(request=request) if request.code_challenge is not None: + request_info["code_challenge"] = request.code_challenge + # OPTIONAL, defaults to "plain" if not present in the request. if request.code_challenge_method is None: request.code_challenge_method = "plain" if request.code_challenge_method not in self._code_challenge_methods: raise errors.UnsupportedCodeChallengeMethodError(request=request) + request_info["code_challenge_method"] = request.code_challenge_method # OPTIONAL. The scope of the access request as described by Section 3.3 # https://tools.ietf.org/html/rfc6749#section-3.3 diff --git a/tests/oauth2/rfc6749/grant_types/test_authorization_code.py b/tests/oauth2/rfc6749/grant_types/test_authorization_code.py index 00e2b6d9..2c9db3c5 100644 --- a/tests/oauth2/rfc6749/grant_types/test_authorization_code.py +++ b/tests/oauth2/rfc6749/grant_types/test_authorization_code.py @@ -215,8 +215,10 @@ def test_pkce_default_method(self): self.mock_validator.is_pkce_required.return_value = required self.request.code_challenge = "present" _, ri = self.auth.validate_authorization_request(self.request) - self.assertIsNotNone(ri["request"].code_challenge_method) - self.assertEqual(ri["request"].code_challenge_method, "plain") + self.assertIn("code_challenge", ri) + self.assertIn("code_challenge_method", ri) + self.assertEqual(ri["code_challenge"], "present") + self.assertEqual(ri["code_challenge_method"], "plain") def test_pkce_wrong_method(self): for required in [True, False]: From 5405ca4cae31146ce2d2c6860b0c46dbbbe879c9 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 29 Apr 2019 10:11:24 +0200 Subject: [PATCH 196/237] Fix docstring about return value --- oauthlib/openid/connect/core/request_validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py index 7ce7e170..eebab550 100644 --- a/oauthlib/openid/connect/core/request_validator.py +++ b/oauthlib/openid/connect/core/request_validator.py @@ -56,7 +56,7 @@ def get_authorization_code_nonce(self, client_id, code, redirect_uri, request): :param client_id: Unicode client identifier :param code: Unicode authorization code grant :param redirect_uri: Unicode absolute URI - :return: A list of scope + :return: Unicode nonce Method is used by: - Authorization Token Grant Dispatcher From 247c89e13bdd017b99f22b154e521084df53d2f0 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 29 Apr 2019 10:12:10 +0200 Subject: [PATCH 197/237] Fix typo gave/have --- oauthlib/openid/connect/core/request_validator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py index eebab550..344fd7d7 100644 --- a/oauthlib/openid/connect/core/request_validator.py +++ b/oauthlib/openid/connect/core/request_validator.py @@ -24,7 +24,7 @@ def get_authorization_code_scopes(self, client_id, code, redirect_uri, request): id_token in token response only based on authorization code scopes. Only code param should be sufficient to retrieve grant code from - any storage you are using, `client_id` and `redirect_uri` can gave a + any storage you are using, `client_id` and `redirect_uri` can have a blank value `""` don't forget to check it before using those values in a select query if a database is used. @@ -49,7 +49,7 @@ def get_authorization_code_nonce(self, client_id, code, redirect_uri, request): case-sensitive string. Only code param should be sufficient to retrieve grant code from - any storage you are using, `client_id` and `redirect_uri` can gave a + any storage you are using, `client_id` and `redirect_uri` can have a blank value `""` don't forget to check it before using those values in a select query if a database is used. From d4d3f1088dc943a83641c9e86b7a09d98f6adce8 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 29 Apr 2019 10:20:39 +0200 Subject: [PATCH 198/237] Removed wrong assumption from copy/paste of get_autho.._scopes. This function should always have a good client_id and redirect_uri, because it is called after validate_token_request() --- oauthlib/openid/connect/core/request_validator.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py index 344fd7d7..d96c9efd 100644 --- a/oauthlib/openid/connect/core/request_validator.py +++ b/oauthlib/openid/connect/core/request_validator.py @@ -49,9 +49,8 @@ def get_authorization_code_nonce(self, client_id, code, redirect_uri, request): case-sensitive string. Only code param should be sufficient to retrieve grant code from - any storage you are using, `client_id` and `redirect_uri` can have a - blank value `""` don't forget to check it before using those values - in a select query if a database is used. + any storage you are using. However, `client_id` and `redirect_uri` + have been validated and can be used also. :param client_id: Unicode client identifier :param code: Unicode authorization code grant From 75cc688ed923724e1bfe20a2a030ee92a151095e Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 30 Apr 2019 16:18:12 +0200 Subject: [PATCH 199/237] Add hooks to highlight the possibilities of the framework The grey color has been used to show that's optional, and a loop arrow to represent that multiple hooks can be stacked. We can distinctly see three kind of hooks: 1) pre/post+token/auth 2) generate access/refresh tokens 3) code/token modifiers. Also, I have added the optional RequestValidator.rotate_refresh_token callback. --- docs/oauth2/oauth2provider-server.dot | 87 +++++++++++++++++++++------ 1 file changed, 69 insertions(+), 18 deletions(-) diff --git a/docs/oauth2/oauth2provider-server.dot b/docs/oauth2/oauth2provider-server.dot index ec240787..934bd20a 100644 --- a/docs/oauth2/oauth2provider-server.dot +++ b/docs/oauth2/oauth2provider-server.dot @@ -5,6 +5,7 @@ digraph oauthlib { webapi_ : oauthlib entry/exit points in shape=hexagon if_ : internal conditions r_ : used when returning from two functions into one for improving clarity + h_ : callbacks/hooks available but not required */ center="1" edge [ style=bold ]; @@ -62,6 +63,7 @@ digraph oauthlib { f_is_within_original_scope [ label="{{is_within_original_scope|{refresh_scopes|refresh_token|request}}|{True|False}}"; ]; f_validate_user [ label="{{validate_user|{username|password|client|request}}|{True|False}}"; ]; f_introspect_token [ label="{{introspect_token|{token|token_type_hint|request}}|{\{claims\}|None}}"; ]; + f_rotate_refresh_token [ label="{{rotate_refresh_token|{request}}|{True|False}}"; ]; } /* OAuthlib Conditions */ @@ -115,11 +117,41 @@ digraph oauthlib { f_is_within_original_scope; } + { + node [ shape=record,color=grey ]; + edge [ color=grey ]; + + h_pre_auth [ label="{{pre_auth|request}|\{credentials\}}}"; ]; + h_post_auth [ label="{{post_auth|request}|\{credentials\}}}"; ]; + h_pre_token [ label="{{pre_token|request}|}}"; ]; + h_pre_token_password [ label="{{pre_token|request}|}}"; ]; + h_pre_token_implicit [ label="{{pre_token|request}|}}"; ]; + h_post_token [ label="{{post_token|request}|}}"; ]; + h_token_modifiers [ label="{{token_modifiers|{token|token_handler|request}}|\{token\}}}"; ]; + h_code_modifiers [ label="{{code_modifiers|{grant|token_handler|request}}|\{grant\}}}"; ]; + h_generate_access_token [ label="{{generate_access_token|request}|\{access token\}}}"; ]; + h_generate_refresh_token [ label="{{generate_refresh_token|request}|\{refresh token\}}}"; ]; + + h_pre_auth:resp:se -> h_pre_auth:arg:ne; + h_post_auth:resp:se -> h_post_auth:arg:ne; + h_pre_token:resp:se -> h_pre_token:arg:ne; + h_pre_token_password:resp:se -> h_pre_token_password:arg:ne; + h_pre_token_implicit:resp:se -> h_pre_token_implicit:arg:ne; + h_post_token:resp:se -> h_post_token:arg:ne; + h_token_modifiers:resp:se -> h_token_modifiers:arg:ne; + h_code_modifiers:resp:se -> h_code_modifiers:arg:ne; + } + { + rank = same; + h_token_modifiers; + h_code_modifiers; + } + /* Authorization Code - Access Token Request */ { edge [ color=darkgreen ]; - endpoint_token:authorization_code:s -> f_client_authentication_required; + endpoint_token:authorization_code:s -> h_pre_token -> f_client_authentication_required; f_client_authentication_required:true:s -> f_authenticate_client; f_client_authentication_required:false:s -> f_authenticate_client_id; f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ]; @@ -134,8 +166,12 @@ digraph oauthlib { if_redirect_uri_missing -> f_get_default_redirect_uri; f_get_default_redirect_uri:redirect_uri:s -> f_confirm_redirect_uri; - f_confirm_redirect_uri:true:s -> f_save_bearer_token; - f_save_bearer_token -> f_invalidate_authorization_code; + f_confirm_redirect_uri:true:s -> h_post_token; + + h_post_token -> h_generate_access_token -> f_rotate_refresh_token; + f_rotate_refresh_token:true:s -> h_generate_refresh_token -> h_token_modifiers; + f_rotate_refresh_token:false:s -> h_token_modifiers; + h_token_modifiers -> f_save_bearer_token -> f_invalidate_authorization_code -> webapi_response; } /* Authorization Code - Authorization Request */ @@ -149,8 +185,9 @@ digraph oauthlib { if_redirect_uri_present -> f_validate_redirect_uri; if_redirect_uri_missing -> f_get_default_redirect_uri; - f_validate_redirect_uri:true:s -> f_validate_response_type; - f_get_default_redirect_uri:redirect_uri:s -> f_validate_response_type; + f_validate_redirect_uri:true:s -> h_pre_auth; + f_get_default_redirect_uri:redirect_uri:s -> h_pre_auth; + h_pre_auth -> f_validate_response_type; f_validate_response_type:true:s -> f_is_pkce_required; f_is_pkce_required:true:s -> if_code_challenge; f_is_pkce_required:false:s -> f_validate_scopes; @@ -158,7 +195,8 @@ digraph oauthlib { if_code_challenge -> f_validate_scopes [ label="present" ]; if_code_challenge -> e_normal [ label="missing",style=dashed ]; - f_validate_scopes:true:s -> f_save_authorization_code; + f_validate_scopes:true:s -> h_post_auth; + h_post_auth -> h_code_modifiers -> f_save_authorization_code; f_save_authorization_code -> webapi_response; } @@ -173,10 +211,13 @@ digraph oauthlib { if_redirect_uri_present -> f_validate_redirect_uri; if_redirect_uri_missing -> f_get_default_redirect_uri; - f_validate_redirect_uri:true:s -> f_validate_response_type; - f_get_default_redirect_uri:redirect_uri:s -> f_validate_response_type; + f_validate_redirect_uri:true:s -> h_pre_auth; + f_get_default_redirect_uri:redirect_uri:s -> h_pre_auth; + h_pre_auth -> h_pre_token_implicit -> f_validate_response_type; + f_validate_response_type:true:s -> f_validate_scopes; - f_validate_scopes:true:s -> f_save_bearer_token; + f_validate_scopes:true:s -> h_post_auth -> h_post_token -> + h_generate_access_token -> h_token_modifiers -> f_save_bearer_token -> webapi_response; } @@ -189,15 +230,19 @@ digraph oauthlib { f_client_authentication_required:false:s -> f_authenticate_client_id; f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ]; f_authenticate_client_id:true:s -> r_client_authenticated [ arrowhead=none ]; - r_client_authenticated -> f_validate_user; + r_client_authenticated -> h_pre_token_password -> f_validate_user; f_validate_user:true:s -> f_validate_grant_type; f_validate_grant_type:true:s -> if_scopes; if_scopes -> f_validate_scopes [ label="present" ]; if_scopes -> f_get_default_scopes [ label="missing" ]; - f_validate_scopes:true:s -> f_save_bearer_token; - f_get_default_scopes -> f_save_bearer_token; + f_validate_scopes:true:s -> h_post_token; + f_get_default_scopes -> h_post_token; + + h_post_token -> h_generate_access_token -> f_rotate_refresh_token; + f_rotate_refresh_token:true:s -> h_generate_refresh_token -> h_token_modifiers; + f_rotate_refresh_token:false:s -> h_token_modifiers -> f_save_bearer_token -> webapi_response; } @@ -205,10 +250,13 @@ digraph oauthlib { { edge [ color=blue ]; - endpoint_token:client_credentials:s -> f_authenticate_client; + endpoint_token:client_credentials:s -> h_pre_token -> f_authenticate_client; + f_authenticate_client:true:s -> f_validate_grant_type; f_validate_grant_type:true:s -> f_validate_scopes; - f_validate_scopes:true:s -> f_save_bearer_token; + f_validate_scopes:true:s -> h_post_token; + + h_post_token -> h_generate_access_token -> h_token_modifiers -> f_save_bearer_token -> webapi_response; } @@ -216,7 +264,7 @@ digraph oauthlib { { edge [ color=brown ]; - endpoint_token:refresh_token:s -> f_client_authentication_required; + endpoint_token:refresh_token:s -> h_pre_token -> f_client_authentication_required; f_client_authentication_required:true:s -> f_authenticate_client; f_client_authentication_required:false:s -> f_authenticate_client_id; f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ]; @@ -227,9 +275,12 @@ digraph oauthlib { f_validate_refresh_token:true:s -> f_get_original_scopes; f_get_original_scopes -> if_all; if_all -> f_is_within_original_scope [ label="True" ]; - if_all -> f_save_bearer_token [ label="False" ]; - f_is_within_original_scope:true:s -> f_save_bearer_token; - f_save_bearer_token -> webapi_response; + if_all -> h_post_token [ label="False" ]; + f_is_within_original_scope:true:s -> h_post_token; + h_post_token -> h_generate_access_token -> f_rotate_refresh_token; + f_rotate_refresh_token:true:s -> h_generate_refresh_token -> h_token_modifiers; + f_rotate_refresh_token:false:s -> h_token_modifiers; + h_token_modifiers -> f_save_bearer_token -> webapi_response; } /* Introspect Endpoint */ From b714c05bcec2e254bbe07298f03f3def7de9179b Mon Sep 17 00:00:00 2001 From: ume Date: Wed, 1 May 2019 00:18:08 +0900 Subject: [PATCH 200/237] token_type should be case insensitive --- oauthlib/oauth2/rfc6749/tokens.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index 79739238..3587af43 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -254,7 +254,7 @@ def get_token_from_header(request): if 'Authorization' in request.headers: split_header = request.headers.get('Authorization').split() - if len(split_header) == 2 and split_header[0] == 'Bearer': + if len(split_header) == 2 and split_header[0].lower() == 'bearer': token = split_header[1] else: token = request.access_token @@ -353,7 +353,7 @@ def estimate_type(self, request): :param request: OAuthlib request. :type request: oauthlib.common.Request """ - if request.headers.get('Authorization', '').split(' ')[0] == 'Bearer': + if request.headers.get('Authorization', '').split(' ')[0].lower() == 'bearer': return 9 elif request.access_token is not None: return 5 From 73092d039fa67a88d0989e7bf0ae7d0044a0bdc6 Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Tue, 30 Apr 2019 12:50:43 -0400 Subject: [PATCH 201/237] Handle null value in expires_in field in JSON handler Closes #672 --- oauthlib/oauth2/rfc6749/parameters.py | 5 ++++- tests/oauth2/rfc6749/test_parameters.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 6b9d6306..f8d42dbb 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -419,7 +419,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/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py index c42f516c..b9fda9c0 100644 --- a/tests/oauth2/rfc6749/test_parameters.py +++ b/tests/oauth2/rfc6749/test_parameters.py @@ -102,6 +102,15 @@ def setUp(self): ' "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 +144,13 @@ def setUp(self): '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, @@ -209,6 +225,8 @@ def test_json_token_response(self): 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): From f037c1153e7e8e22299e9169b8d765164855b246 Mon Sep 17 00:00:00 2001 From: Jordan Gardner Date: Wed, 24 Apr 2019 15:09:51 -0600 Subject: [PATCH 202/237] Add case-insensitive headers to oauth1 BaseEndpoint --- oauthlib/oauth1/rfc5849/endpoints/base.py | 4 ++-- tests/oauth1/rfc5849/endpoints/test_base.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/oauthlib/oauth1/rfc5849/endpoints/base.py b/oauthlib/oauth1/rfc5849/endpoints/base.py index 9702939b..ecf8a50d 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/base.py +++ b/oauthlib/oauth1/rfc5849/endpoints/base.py @@ -10,7 +10,7 @@ import time -from oauthlib.common import Request, generate_token +from oauthlib.common import CaseInsensitiveDict, Request, generate_token from .. import (CONTENT_TYPE_FORM_URLENCODED, SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_BODY, @@ -67,7 +67,7 @@ def _get_signature_type_and_params(self, request): def _create_request(self, uri, http_method, body, headers): # Only include body data from x-www-form-urlencoded requests - headers = headers or {} + headers = CaseInsensitiveDict(headers or {}) if ("Content-Type" in headers and CONTENT_TYPE_FORM_URLENCODED in headers["Content-Type"]): request = Request(uri, http_method, body, headers) diff --git a/tests/oauth1/rfc5849/endpoints/test_base.py b/tests/oauth1/rfc5849/endpoints/test_base.py index 60f7860e..795ddeee 100644 --- a/tests/oauth1/rfc5849/endpoints/test_base.py +++ b/tests/oauth1/rfc5849/endpoints/test_base.py @@ -4,7 +4,7 @@ from mock import MagicMock -from oauthlib.common import safe_string_equals +from oauthlib.common import CaseInsensitiveDict, safe_string_equals from oauthlib.oauth1 import Client, RequestValidator from oauthlib.oauth1.rfc5849 import (SIGNATURE_HMAC, SIGNATURE_PLAINTEXT, SIGNATURE_RSA, errors) @@ -179,6 +179,17 @@ def test_oauth_timestamp(self): self.assertRaises(errors.InvalidRequestError, e._check_mandatory_parameters, r) + def test_case_insensitive_headers(self): + """Ensure headers are case-insensitive""" + v = RequestValidator() + e = BaseEndpoint(v) + r = e._create_request('https://a.b', 'POST', + ('oauth_signature=a&oauth_consumer_key=b&oauth_nonce=c&' + 'oauth_version=1.0&oauth_signature_method=RSA-SHA1&' + 'oauth_timestamp=123456789a'), + URLENCODED) + self.assertIsInstance(r.headers, CaseInsensitiveDict) + def test_signature_method_validation(self): """Ensure valid signature method is used.""" From d4a3a419b1aed1a370b29785d3121f4877d4cd54 Mon Sep 17 00:00:00 2001 From: "Y.Umezaki" Date: Tue, 7 May 2019 18:53:06 +0900 Subject: [PATCH 203/237] Add token tests from #491 --- tests/oauth2/rfc6749/test_tokens.py | 51 +++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/tests/oauth2/rfc6749/test_tokens.py b/tests/oauth2/rfc6749/test_tokens.py index 061754f6..95e8f6ce 100644 --- a/tests/oauth2/rfc6749/test_tokens.py +++ b/tests/oauth2/rfc6749/test_tokens.py @@ -1,10 +1,14 @@ from __future__ import absolute_import, unicode_literals +import mock + +from oauthlib.common import Request from oauthlib.oauth2.rfc6749.tokens import ( - prepare_mac_header, - prepare_bearer_headers, + BearerToken, prepare_bearer_body, + prepare_bearer_headers, prepare_bearer_uri, + prepare_mac_header, ) from ...unittest import TestCase @@ -98,3 +102,46 @@ def test_prepare_bearer_request(self): self.assertEqual(prepare_bearer_headers(self.token), self.bearer_headers) self.assertEqual(prepare_bearer_body(self.token), self.bearer_body) self.assertEqual(prepare_bearer_uri(self.token, uri=self.uri), self.bearer_uri) + + def test_fake_bearer_is_not_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + + for fake_header in self.fake_bearer_headers: + request = Request("/", headers=fake_header) + result = BearerToken(request_validator=request_validator).validate_request( + request + ) + + self.assertFalse(result) + + def test_header_with_multispaces_is_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + + request = Request("/", headers=self.valid_header_with_multiple_spaces) + result = BearerToken(request_validator=request_validator).validate_request( + request + ) + + self.assertTrue(result) + + def test_estimate_type_with_fake_header_returns_type_0(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + + for fake_header in self.fake_bearer_headers: + request = Request("/", headers=fake_header) + result = BearerToken(request_validator=request_validator).estimate_type( + request + ) + + if ( + fake_header["Authorization"].count(" ") == 2 + and fake_header["Authorization"].split()[0] == "Bearer" + ): + # If we're dealing with the header containing 2 spaces, it will be recognized + # as a Bearer valid header, the token itself will be invalid by the way. + self.assertEqual(result, 9) + else: + self.assertEqual(result, 0) From 056383bf96892b7428b5de17bb2011374fe1c7bf Mon Sep 17 00:00:00 2001 From: "Y.Umezaki" Date: Tue, 7 May 2019 19:10:14 +0900 Subject: [PATCH 204/237] Add valid testcase --- tests/oauth2/rfc6749/test_tokens.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/oauth2/rfc6749/test_tokens.py b/tests/oauth2/rfc6749/test_tokens.py index 95e8f6ce..e6f49b10 100644 --- a/tests/oauth2/rfc6749/test_tokens.py +++ b/tests/oauth2/rfc6749/test_tokens.py @@ -68,6 +68,7 @@ class TokenTest(TestCase): bearer_headers = { 'Authorization': 'Bearer vF9dft4qmT' } + valid_bearer_header_lowercase = {"Authorization": "bearer vF9dft4qmT"} fake_bearer_headers = [ {'Authorization': 'Beaver vF9dft4qmT'}, {'Authorization': 'BeavervF9dft4qmT'}, @@ -103,6 +104,26 @@ def test_prepare_bearer_request(self): self.assertEqual(prepare_bearer_body(self.token), self.bearer_body) self.assertEqual(prepare_bearer_uri(self.token, uri=self.uri), self.bearer_uri) + def test_valid_bearer_is_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + + request = Request("/", headers=self.bearer_headers) + result = BearerToken(request_validator=request_validator).validate_request( + request + ) + self.assertTrue(result) + + def test_lowercase_bearer_is_validated(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + + request = Request("/", headers=self.valid_bearer_header_lowercase) + result = BearerToken(request_validator=request_validator).validate_request( + request + ) + self.assertTrue(result) + def test_fake_bearer_is_not_validated(self): request_validator = mock.MagicMock() request_validator.validate_bearer_token = self._mocked_validate_bearer_token @@ -126,6 +147,13 @@ def test_header_with_multispaces_is_validated(self): self.assertTrue(result) + def test_estimate_type(self): + request_validator = mock.MagicMock() + request_validator.validate_bearer_token = self._mocked_validate_bearer_token + request = Request("/", headers=self.bearer_headers) + result = BearerToken(request_validator=request_validator).estimate_type(request) + self.assertEqual(result, 9) + def test_estimate_type_with_fake_header_returns_type_0(self): request_validator = mock.MagicMock() request_validator.validate_bearer_token = self._mocked_validate_bearer_token From a8eaf06eabedba10b5bc928a831e71682c9e88ee Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 13 May 2019 11:27:19 +0200 Subject: [PATCH 205/237] Removed duplicated code for oauth2.BaseEndpoint --- oauthlib/oauth2/rfc6749/__init__.py | 50 ++--------------------------- 1 file changed, 2 insertions(+), 48 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/__init__.py b/oauthlib/oauth2/rfc6749/__init__.py index aff0ed88..1a4128c5 100644 --- a/oauthlib/oauth2/rfc6749/__init__.py +++ b/oauthlib/oauth2/rfc6749/__init__.py @@ -11,56 +11,10 @@ import functools import logging +from .endpoints.base import BaseEndpoint +from .endpoints.base import catch_errors_and_unavailability from .errors import TemporarilyUnavailableError, ServerError from .errors import FatalClientError, OAuth2Error log = logging.getLogger(__name__) - - -class BaseEndpoint(object): - - def __init__(self): - self._available = True - self._catch_errors = False - - @property - def available(self): - return self._available - - @available.setter - def available(self, available): - self._available = available - - @property - def catch_errors(self): - return self._catch_errors - - @catch_errors.setter - def catch_errors(self, catch_errors): - self._catch_errors = catch_errors - - -def catch_errors_and_unavailability(f): - @functools.wraps(f) - def wrapper(endpoint, uri, *args, **kwargs): - if not endpoint.available: - e = TemporarilyUnavailableError() - log.info('Endpoint unavailable, ignoring request %s.' % uri) - return {}, e.json, 503 - - if endpoint.catch_errors: - try: - return f(endpoint, uri, *args, **kwargs) - except OAuth2Error: - raise - except FatalClientError: - raise - except Exception as e: - error = ServerError() - log.warning( - 'Exception caught while processing request, %s.' % e) - return {}, error.json, 500 - else: - return f(endpoint, uri, *args, **kwargs) - return wrapper From 0821ab7467fec977cc8dbe357435d03861c16027 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 13 May 2019 11:36:20 +0200 Subject: [PATCH 206/237] Add UserInfoEndpoint to the OIDC Provider support. --- docs/oauth2/oidc/validator.rst | 1 + oauthlib/openid/__init__.py | 1 + .../openid/connect/core/endpoints/__init__.py | 1 + .../connect/core/endpoints/pre_configured.py | 4 +- .../openid/connect/core/endpoints/userinfo.py | 102 ++++++++++++++++++ .../openid/connect/core/request_validator.py | 42 ++++++++ .../core/endpoints/test_userinfo_endpoint.py | 70 ++++++++++++ 7 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 oauthlib/openid/connect/core/endpoints/userinfo.py create mode 100644 tests/openid/connect/core/endpoints/test_userinfo_endpoint.py diff --git a/docs/oauth2/oidc/validator.rst b/docs/oauth2/oidc/validator.rst index 7a6f5744..17f58255 100644 --- a/docs/oauth2/oidc/validator.rst +++ b/docs/oauth2/oidc/validator.rst @@ -20,6 +20,7 @@ Into from oauthlib.openid import RequestValidator Then, you have to implement the new RequestValidator methods as shown below. +Note that a new UserInfo endpoint is defined and need a new controller into your webserver. RequestValidator Extension ---------------------------------------------------- diff --git a/oauthlib/openid/__init__.py b/oauthlib/openid/__init__.py index 7f1a8767..8157c297 100644 --- a/oauthlib/openid/__init__.py +++ b/oauthlib/openid/__init__.py @@ -7,4 +7,5 @@ from __future__ import absolute_import, unicode_literals from .connect.core.endpoints import Server +from .connect.core.endpoints import UserInfoEndpoint from .connect.core.request_validator import RequestValidator diff --git a/oauthlib/openid/connect/core/endpoints/__init__.py b/oauthlib/openid/connect/core/endpoints/__init__.py index 719f883c..528841f4 100644 --- a/oauthlib/openid/connect/core/endpoints/__init__.py +++ b/oauthlib/openid/connect/core/endpoints/__init__.py @@ -9,3 +9,4 @@ from __future__ import absolute_import, unicode_literals from .pre_configured import Server +from .userinfo import UserInfoEndpoint diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py index 6367847b..fde2739e 100644 --- a/oauthlib/openid/connect/core/endpoints/pre_configured.py +++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py @@ -34,10 +34,11 @@ AuthorizationTokenGrantDispatcher ) from ..tokens import JWTToken +from .userinfo import UserInfoEndpoint class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint, - ResourceEndpoint, RevocationEndpoint): + ResourceEndpoint, RevocationEndpoint, UserInfoEndpoint): """An all-in-one endpoint featuring all four major grant types.""" @@ -105,3 +106,4 @@ def __init__(self, request_validator, token_expires_in=None, token_types={'Bearer': bearer, 'JWT': jwt}) RevocationEndpoint.__init__(self, request_validator) IntrospectEndpoint.__init__(self, request_validator) + UserInfoEndpoint.__init__(self, request_validator) diff --git a/oauthlib/openid/connect/core/endpoints/userinfo.py b/oauthlib/openid/connect/core/endpoints/userinfo.py new file mode 100644 index 00000000..7a39f76b --- /dev/null +++ b/oauthlib/openid/connect/core/endpoints/userinfo.py @@ -0,0 +1,102 @@ +""" +oauthlib.openid.connect.core.endpoints.userinfo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of userinfo endpoint. +""" +from __future__ import absolute_import, unicode_literals + +import json +import logging + +from oauthlib.common import Request +from oauthlib.common import unicode_type +from oauthlib.oauth2.rfc6749.endpoints.base import BaseEndpoint +from oauthlib.oauth2.rfc6749.endpoints.base import catch_errors_and_unavailability +from oauthlib.oauth2.rfc6749.tokens import BearerToken +from oauthlib.oauth2.rfc6749 import errors + + +log = logging.getLogger(__name__) + + +class UserInfoEndpoint(BaseEndpoint): + """Authorizes access to userinfo resource. + """ + def __init__(self, request_validator): + self.bearer = BearerToken(request_validator, None, None, None) + self.request_validator = request_validator + BaseEndpoint.__init__(self) + + @catch_errors_and_unavailability + def create_userinfo_response(self, uri, http_method='GET', body=None, headers=None): + """Validate BearerToken and return userinfo from RequestValidator + + The UserInfo Endpoint MUST return a + content-type header to indicate which format is being returned. The + content-type of the HTTP response MUST be application/json if the + response body is a text JSON object; the response body SHOULD be encoded + using UTF-8. + """ + request = Request(uri, http_method, body, headers) + request.scopes = ["openid"] + self.validate_userinfo_request(request) + + claims = self.request_validator.get_userinfo_claims(request) + if claims is None: + log.error('Userinfo MUST have claims for %r.', request) + raise errors.ServerError(status_code=500) + + if isinstance(claims, dict): + resp_headers = { + 'Content-Type': 'application/json' + } + if "sub" not in claims: + log.error('Userinfo MUST have "sub" for %r.', request) + raise errors.ServerError(status_code=500) + body = json.dumps(claims) + elif isinstance(claims, unicode_type): + resp_headers = { + 'Content-Type': 'application/jwt' + } + body = claims + else: + log.error('Userinfo return unknown response for %r.', request) + raise errors.ServerError(status_code=500) + log.debug('Userinfo access valid for %r.', request) + return resp_headers, body, 200 + + def validate_userinfo_request(self, request): + """Ensure the request is valid. + + 5.3.1. UserInfo Request + The Client sends the UserInfo Request using either HTTP GET or HTTP + POST. The Access Token obtained from an OpenID Connect Authentication + Request MUST be sent as a Bearer Token, per Section 2 of OAuth 2.0 + Bearer Token Usage [RFC6750]. + + It is RECOMMENDED that the request use the HTTP GET method and the + Access Token be sent using the Authorization header field. + + The following is a non-normative example of a UserInfo Request: + + GET /userinfo HTTP/1.1 + Host: server.example.com + Authorization: Bearer SlAV32hkKG + + 5.3.3. UserInfo Error Response + When an error condition occurs, the UserInfo Endpoint returns an Error + Response as defined in Section 3 of OAuth 2.0 Bearer Token Usage + [RFC6750]. (HTTP errors unrelated to RFC 6750 are returned to the User + Agent using the appropriate HTTP status code.) + + The following is a non-normative example of a UserInfo Error Response: + + HTTP/1.1 401 Unauthorized + WWW-Authenticate: Bearer error="invalid_token", + error_description="The Access Token expired" + """ + if not self.bearer.validate_request(request): + raise errors.InvalidTokenError() + if "openid" not in request.scopes: + raise errors.InsufficientScopeError() diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py index d96c9efd..e853d39c 100644 --- a/oauthlib/openid/connect/core/request_validator.py +++ b/oauthlib/openid/connect/core/request_validator.py @@ -265,3 +265,45 @@ def validate_user_match(self, id_token_hint, scopes, claims, request): - OpenIDConnectHybrid """ raise NotImplementedError('Subclasses must implement this method.') + + def get_userinfo_claims(self, request): + """Return the UserInfo claims in JSON or Signed or Encrypted. + + The UserInfo Claims MUST be returned as the members of a JSON object + unless a signed or encrypted response was requested during Client + Registration. The Claims defined in Section 5.1 can be returned, as can + additional Claims not specified there. + + For privacy reasons, OpenID Providers MAY elect to not return values for + some requested Claims. + + If a Claim is not returned, that Claim Name SHOULD be omitted from the + JSON object representing the Claims; it SHOULD NOT be present with a + null or empty string value. + + The sub (subject) Claim MUST always be returned in the UserInfo + Response. + + Upon receipt of the UserInfo Request, the UserInfo Endpoint MUST return + the JSON Serialization of the UserInfo Response as in Section 13.3 in + the HTTP response body unless a different format was specified during + Registration [OpenID.Registration]. + + If the UserInfo Response is signed and/or encrypted, then the Claims are + returned in a JWT and the content-type MUST be application/jwt. The + response MAY be encrypted without also being signed. If both signing and + encryption are requested, the response MUST be signed then encrypted, + with the result being a Nested JWT, as defined in [JWT]. + + If signed, the UserInfo Response SHOULD contain the Claims iss (issuer) + and aud (audience) as members. The iss value SHOULD be the OP's Issuer + Identifier URL. The aud value SHOULD be or include the RP's Client ID + value. + + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :rtype: Claims as a dict OR JWT/JWS/JWE as a string + + Method is used by: + UserInfoEndpoint + """ diff --git a/tests/openid/connect/core/endpoints/test_userinfo_endpoint.py b/tests/openid/connect/core/endpoints/test_userinfo_endpoint.py new file mode 100644 index 00000000..4593d790 --- /dev/null +++ b/tests/openid/connect/core/endpoints/test_userinfo_endpoint.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +import mock +import json + +from oauthlib.openid import RequestValidator +from oauthlib.openid import UserInfoEndpoint +from oauthlib.oauth2.rfc6749 import errors + +from tests.unittest import TestCase + + +def set_scopes_valid(token, scopes, request): + request.scopes = ["openid", "bar"] + return True + + +class UserInfoEndpointTest(TestCase): + def setUp(self): + self.claims = { + "sub": "john", + "fruit": "banana" + } + # Can't use MagicMock/wraps below. + # Triggers error when endpoint copies to self.bearer.request_validator + self.validator = RequestValidator() + self.validator.validate_bearer_token = mock.Mock() + self.validator.validate_bearer_token.side_effect = set_scopes_valid + self.validator.get_userinfo_claims = mock.Mock() + self.validator.get_userinfo_claims.return_value = self.claims + self.endpoint = UserInfoEndpoint(self.validator) + + self.uri = 'should_not_matter' + self.headers = { + 'Authorization': 'Bearer eyJxx' + } + + def test_userinfo_no_auth(self): + self.endpoint.create_userinfo_response(self.uri) + + def test_userinfo_wrong_auth(self): + self.headers['Authorization'] = 'Basic foifoifoi' + self.endpoint.create_userinfo_response(self.uri, headers=self.headers) + + def test_userinfo_token_expired(self): + self.validator.validate_bearer_token.return_value = False + self.endpoint.create_userinfo_response(self.uri, headers=self.headers) + + def test_userinfo_token_no_openid_scope(self): + def set_scopes_invalid(token, scopes, request): + request.scopes = ["foo", "bar"] + return True + self.validator.validate_bearer_token.side_effect = set_scopes_invalid + with self.assertRaises(errors.InsufficientScopeError) as context: + self.endpoint.create_userinfo_response(self.uri) + + def test_userinfo_json_response(self): + h, b, s = self.endpoint.create_userinfo_response(self.uri) + self.assertEqual(s, 200) + body_json = json.loads(b) + self.assertEqual(self.claims, body_json) + self.assertEqual("application/json", h['Content-Type']) + + def test_userinfo_jwt_response(self): + self.validator.get_userinfo_claims.return_value = "eyJzzzzz" + h, b, s = self.endpoint.create_userinfo_response(self.uri) + self.assertEqual(s, 200) + self.assertEqual(b, "eyJzzzzz") + self.assertEqual("application/jwt", h['Content-Type']) From f25b9133646d0120f6221e9d7cbf16e747dd6aa7 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 13 May 2019 14:53:41 +0200 Subject: [PATCH 207/237] Updated bandit baseline after review --- bandit.json | 1184 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1179 insertions(+), 5 deletions(-) diff --git a/bandit.json b/bandit.json index 02e15a83..4d3bfe19 100644 --- a/bandit.json +++ b/bandit.json @@ -1,21 +1,1034 @@ { "errors": [], - "generated_at": "2018-12-13T10:39:37Z", + "generated_at": "2019-05-13T12:51:49Z", + "metrics": { + "_totals": { + "CONFIDENCE.HIGH": 3.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 10.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 12.0, + "SEVERITY.MEDIUM": 1.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 8338, + "nosec": 0 + }, + "oauthlib/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 25, + "nosec": 0 + }, + "oauthlib/common.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 337, + "nosec": 0 + }, + "oauthlib/oauth1/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 16, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/__init__.py": { + "CONFIDENCE.HIGH": 1.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 1.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 230, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 8, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/access_token.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 152, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/authorization.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 135, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/base.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 142, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/pre_configured.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 10, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/request_token.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 141, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/resource.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 97, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/signature_only.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 53, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/errors.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 58, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/parameters.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 75, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/request_validator.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 630, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/signature.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 379, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/utils.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 61, + "nosec": 0 + }, + "oauthlib/oauth2/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 33, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 14, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/clients/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 13, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/clients/backend_application.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 56, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/clients/base.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 3.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 3.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 384, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/clients/legacy_application.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 67, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/clients/mobile_application.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 140, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/clients/service_application.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 144, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/clients/web_application.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 165, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 18, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/authorization.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 85, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/base.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 71, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/introspect.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 98, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/metadata.py": { + "CONFIDENCE.HIGH": 2.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 2.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 182, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 5.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 5.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 189, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/resource.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 65, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/revocation.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 96, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/token.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 76, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/errors.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 311, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/grant_types/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 10, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/grant_types/authorization_code.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 389, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/grant_types/base.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 199, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/grant_types/client_credentials.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 96, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/grant_types/implicit.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 259, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/grant_types/refresh_token.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 102, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 156, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/parameters.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 1.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 1.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 335, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/request_validator.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 504, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/tokens.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 277, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/utils.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 70, + "nosec": 0 + }, + "oauthlib/openid/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 8, + "nosec": 0 + }, + "oauthlib/openid/connect/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 0, + "nosec": 0 + }, + "oauthlib/openid/connect/core/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 0, + "nosec": 0 + }, + "oauthlib/openid/connect/core/endpoints/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 9, + "nosec": 0 + }, + "oauthlib/openid/connect/core/endpoints/pre_configured.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 1.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 1.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 93, + "nosec": 0 + }, + "oauthlib/openid/connect/core/endpoints/userinfo.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 83, + "nosec": 0 + }, + "oauthlib/openid/connect/core/exceptions.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 117, + "nosec": 0 + }, + "oauthlib/openid/connect/core/grant_types/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 15, + "nosec": 0 + }, + "oauthlib/openid/connect/core/grant_types/authorization_code.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 32, + "nosec": 0 + }, + "oauthlib/openid/connect/core/grant_types/base.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 234, + "nosec": 0 + }, + "oauthlib/openid/connect/core/grant_types/dispatchers.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 66, + "nosec": 0 + }, + "oauthlib/openid/connect/core/grant_types/exceptions.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 26, + "nosec": 0 + }, + "oauthlib/openid/connect/core/grant_types/hybrid.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 38, + "nosec": 0 + }, + "oauthlib/openid/connect/core/grant_types/implicit.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 35, + "nosec": 0 + }, + "oauthlib/openid/connect/core/request_validator.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 235, + "nosec": 0 + }, + "oauthlib/openid/connect/core/tokens.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 42, + "nosec": 0 + }, + "oauthlib/signals.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 32, + "nosec": 0 + }, + "oauthlib/tokens/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 0, + "nosec": 0 + }, + "oauthlib/tokens/access_token.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 13, + "nosec": 0 + }, + "oauthlib/tokens/base.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 8, + "nosec": 0 + }, + "oauthlib/tokens/id_token.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 6, + "nosec": 0 + }, + "oauthlib/uri_validate.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 93, + "nosec": 0 + } + }, "results": [ { - "code": "182 if request.body is not None and content_type_eligible:\n183 params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8')))\n184 \n", + "code": "183 if request.body is not None and content_type_eligible:\n184 params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8')))\n185 \n", "filename": "oauthlib/oauth1/rfc5849/__init__.py", "issue_confidence": "HIGH", "issue_severity": "MEDIUM", "issue_text": "Use of insecure MD2, MD4, MD5, or SHA1 hash function.", - "line_number": 183, + "line_number": 184, "line_range": [ - 183 + 184 ], "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html#b303-md5", "test_id": "B303", "test_name": "blacklist" }, + { + "code": "49 \"\"\"\n50 refresh_token_key = 'refresh_token'\n51 \n52 def __init__(self, client_id,\n", + "filename": "oauthlib/oauth2/rfc6749/clients/base.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'refresh_token'", + "line_number": 50, + "line_range": [ + 50, + 51 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b105_hardcoded_password_string.html", + "test_id": "B105", + "test_name": "hardcoded_password_string" + }, + { + "code": "51 \n52 def __init__(self, client_id,\n53 default_token_placement=AUTH_HEADER,\n54 token_type='Bearer',\n55 access_token=None,\n56 refresh_token=None,\n57 mac_key=None,\n58 mac_algorithm=None,\n59 token=None,\n60 scope=None,\n61 state=None,\n62 redirect_url=None,\n63 state_generator=generate_token,\n64 **kwargs):\n65 \"\"\"Initialize a client with commonly used attributes.\n66 \n67 :param client_id: Client identifier given by the OAuth provider upon\n68 registration.\n69 \n70 :param default_token_placement: Tokens can be supplied in the Authorization\n71 header (default), the URL query component (``query``) or the request\n72 body (``body``).\n73 \n74 :param token_type: OAuth 2 token type. Defaults to Bearer. Change this\n75 if you specify the ``access_token`` parameter and know it is of a\n76 different token type, such as a MAC, JWT or SAML token. Can\n77 also be supplied as ``token_type`` inside the ``token`` dict parameter.\n78 \n79 :param access_token: An access token (string) used to authenticate\n80 requests to protected resources. Can also be supplied inside the\n81 ``token`` dict parameter.\n82 \n83 :param refresh_token: A refresh token (string) used to refresh expired\n84 tokens. Can also be supplied inside the ``token`` dict parameter.\n85 \n86 :param mac_key: Encryption key used with MAC tokens.\n87 \n88 :param mac_algorithm: Hashing algorithm for MAC tokens.\n89 \n90 :param token: A dict of token attributes such as ``access_token``,\n91 ``token_type`` and ``expires_at``.\n92 \n93 :param scope: A list of default scopes to request authorization for.\n94 \n95 :param state: A CSRF protection string used during authorization.\n96 \n97 :param redirect_url: The redirection endpoint on the client side to which\n98 the user returns after authorization.\n99 \n100 :param state_generator: A no argument state generation callable. Defaults\n101 to :py:meth:`oauthlib.common.generate_token`.\n102 \"\"\"\n103 \n104 self.client_id = client_id\n105 self.default_token_placement = default_token_placement\n106 self.token_type = token_type\n107 self.access_token = access_token\n108 self.refresh_token = refresh_token\n109 self.mac_key = mac_key\n110 self.mac_algorithm = mac_algorithm\n111 self.token = token or {}\n112 self.scope = scope\n113 self.state_generator = state_generator\n114 self.state = state\n115 self.redirect_url = redirect_url\n116 self.code = None\n117 self.expires_in = None\n118 self._expires_at = None\n119 self.populate_token_attributes(self.token)\n120 \n121 @property\n", + "filename": "oauthlib/oauth2/rfc6749/clients/base.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'Bearer'", + "line_number": 52, + "line_range": [ + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html", + "test_id": "B107", + "test_name": "hardcoded_password_default" + }, + { + "code": "313 \n314 def prepare_token_revocation_request(self, revocation_url, token,\n315 token_type_hint=\"access_token\", body='', callback=None, **kwargs):\n316 \"\"\"Prepare a token revocation request.\n317 \n318 :param revocation_url: Provider token revocation endpoint URL.\n319 \n320 :param token: The access or refresh token to be revoked (string).\n321 \n322 :param token_type_hint: ``\"access_token\"`` (default) or\n323 ``\"refresh_token\"``. This is optional and if you wish to not pass it you\n324 must provide ``token_type_hint=None``.\n325 \n326 :param body:\n327 \n328 :param callback: A jsonp callback such as ``package.callback`` to be invoked\n329 upon receiving the response. Not that it should not include a () suffix.\n330 \n331 :param kwargs: Additional parameters to included in the request.\n332 \n333 :returns: The prepared request tuple with (url, headers, body).\n334 \n335 Note that JSONP request may use GET requests as the parameters will\n336 be added to the request URL query as opposed to the request body.\n337 \n338 An example of a revocation request\n339 \n340 .. code-block: http\n341 \n342 POST /revoke HTTP/1.1\n343 Host: server.example.com\n344 Content-Type: application/x-www-form-urlencoded\n345 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW\n346 \n347 token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token\n348 \n349 An example of a jsonp revocation request\n350 \n351 .. code-block: http\n352 \n353 GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1\n354 Host: server.example.com\n355 Content-Type: application/x-www-form-urlencoded\n356 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW\n357 \n358 and an error response\n359 \n360 .. code-block: http\n361 \n362 package.myCallback({\"error\":\"unsupported_token_type\"});\n363 \n364 Note that these requests usually require client credentials, client_id in\n365 the case for public clients and provider specific authentication\n366 credentials for confidential clients.\n367 \"\"\"\n368 if not is_secure_transport(revocation_url):\n369 raise InsecureTransportError()\n370 \n371 return prepare_token_revocation_request(revocation_url, token,\n372 token_type_hint=token_type_hint, body=body, callback=callback,\n373 **kwargs)\n374 \n375 def parse_request_body_response(self, body, scope=None, **kwargs):\n", + "filename": "oauthlib/oauth2/rfc6749/clients/base.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'access_token'", + "line_number": 314, + "line_range": [ + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321, + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329, + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 337, + 338, + 339, + 340, + 341, + 342, + 343, + 344, + 345, + 346, + 347, + 348, + 349, + 350, + 351, + 352, + 353, + 354, + 355, + 356, + 357, + 358, + 359, + 360, + 361, + 362, + 363, + 364, + 365, + 366, + 367, + 368, + 369, + 370, + 371, + 372, + 373, + 374 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html", + "test_id": "B107", + "test_name": "hardcoded_password_default" + }, { "code": "45 def __init__(self, endpoints, claims={}, raise_errors=True):\n46 assert isinstance(claims, dict)\n47 for endpoint in endpoints:\n", "filename": "oauthlib/oauth2/rfc6749/endpoints/metadata.py", @@ -43,6 +1056,167 @@ "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b101_assert_used.html", "test_id": "B101", "test_name": "assert_used" + }, + { + "code": "70 default_token_type=bearer)\n71 ResourceEndpoint.__init__(self, default_token='Bearer',\n72 token_types={'Bearer': bearer})\n73 RevocationEndpoint.__init__(self, request_validator)\n", + "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'Bearer'", + "line_number": 71, + "line_range": [ + 71, + 72 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", + "test_id": "B106", + "test_name": "hardcoded_password_funcarg" + }, + { + "code": "109 default_token_type=bearer)\n110 ResourceEndpoint.__init__(self, default_token='Bearer',\n111 token_types={'Bearer': bearer})\n112 RevocationEndpoint.__init__(self, request_validator)\n", + "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'Bearer'", + "line_number": 110, + "line_range": [ + 110, + 111 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", + "test_id": "B106", + "test_name": "hardcoded_password_funcarg" + }, + { + "code": "142 default_token_type=bearer)\n143 ResourceEndpoint.__init__(self, default_token='Bearer',\n144 token_types={'Bearer': bearer})\n145 RevocationEndpoint.__init__(self, request_validator,\n", + "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'Bearer'", + "line_number": 143, + "line_range": [ + 143, + 144 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", + "test_id": "B106", + "test_name": "hardcoded_password_funcarg" + }, + { + "code": "181 default_token_type=bearer)\n182 ResourceEndpoint.__init__(self, default_token='Bearer',\n183 token_types={'Bearer': bearer})\n184 RevocationEndpoint.__init__(self, request_validator)\n", + "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'Bearer'", + "line_number": 182, + "line_range": [ + 182, + 183 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", + "test_id": "B106", + "test_name": "hardcoded_password_funcarg" + }, + { + "code": "214 default_token_type=bearer)\n215 ResourceEndpoint.__init__(self, default_token='Bearer',\n216 token_types={'Bearer': bearer})\n217 RevocationEndpoint.__init__(self, request_validator,\n", + "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'Bearer'", + "line_number": 215, + "line_range": [ + 215, + 216 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", + "test_id": "B106", + "test_name": "hardcoded_password_funcarg" + }, + { + "code": "164 \n165 def prepare_token_revocation_request(url, token, token_type_hint=\"access_token\",\n166 callback=None, body='', **kwargs):\n167 \"\"\"Prepare a token revocation request.\n168 \n169 The client constructs the request by including the following parameters\n170 using the \"application/x-www-form-urlencoded\" format in the HTTP request\n171 entity-body:\n172 \n173 :param token: REQUIRED. The token that the client wants to get revoked.\n174 \n175 :param token_type_hint: OPTIONAL. A hint about the type of the token\n176 submitted for revocation. Clients MAY pass this\n177 parameter in order to help the authorization server\n178 to optimize the token lookup. If the server is\n179 unable to locate the token using the given hint, it\n180 MUST extend its search across all of its supported\n181 token types. An authorization server MAY ignore\n182 this parameter, particularly if it is able to detect\n183 the token type automatically.\n184 \n185 This specification defines two values for `token_type_hint`:\n186 \n187 * access_token: An access token as defined in [RFC6749],\n188 `Section 1.4`_\n189 \n190 * refresh_token: A refresh token as defined in [RFC6749],\n191 `Section 1.5`_\n192 \n193 Specific implementations, profiles, and extensions of this\n194 specification MAY define other values for this parameter using the\n195 registry defined in `Section 4.1.2`_.\n196 \n197 .. _`Section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4\n198 .. _`Section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5\n199 .. _`Section 4.1.2`: https://tools.ietf.org/html/rfc7009#section-4.1.2\n200 \n201 \"\"\"\n202 if not is_secure_transport(url):\n203 raise InsecureTransportError()\n204 \n205 params = [('token', token)]\n206 \n207 if token_type_hint:\n208 params.append(('token_type_hint', token_type_hint))\n209 \n210 for k in kwargs:\n211 if kwargs[k]:\n212 params.append((unicode_type(k), kwargs[k]))\n213 \n214 headers = {'Content-Type': 'application/x-www-form-urlencoded'}\n215 \n216 if callback:\n217 params.append(('callback', callback))\n218 return add_params_to_uri(url, params), headers, body\n219 else:\n220 return url, headers, add_params_to_qs(body, params)\n221 \n222 \n223 def parse_authorization_code_response(uri, state=None):\n", + "filename": "oauthlib/oauth2/rfc6749/parameters.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'access_token'", + "line_number": 165, + "line_range": [ + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 174, + 175, + 176, + 177, + 178, + 179, + 180, + 181, + 182, + 183, + 184, + 185, + 186, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 209, + 210, + 211, + 212, + 213, + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html", + "test_id": "B107", + "test_name": "hardcoded_password_default" + }, + { + "code": "104 default_token_type=bearer)\n105 ResourceEndpoint.__init__(self, default_token='Bearer',\n106 token_types={'Bearer': bearer, 'JWT': jwt})\n107 RevocationEndpoint.__init__(self, request_validator)\n", + "filename": "oauthlib/openid/connect/core/endpoints/pre_configured.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'Bearer'", + "line_number": 105, + "line_range": [ + 105, + 106 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", + "test_id": "B106", + "test_name": "hardcoded_password_funcarg" } ] -} +} \ No newline at end of file From acf62ce7c66f87a26f47c460a9934bede6126061 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 13 May 2019 14:54:03 +0200 Subject: [PATCH 208/237] Force bandit python version to be sure no conflict with others --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 1cac71c1..d242791f 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,7 @@ commands= echo setup.py/long description is syntaxly correct [testenv:bandit] +basepython=python3.6 skipsdist=True deps=bandit commands=bandit -b bandit.json -r oauthlib/ From 588abb50010d434c0de5ad9c479d666b7b6ab0bd Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 13 May 2019 15:08:18 +0200 Subject: [PATCH 209/237] Downgrade python to match with Travis --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d242791f..9cd2a9fb 100644 --- a/tox.ini +++ b/tox.ini @@ -28,7 +28,7 @@ commands= echo setup.py/long description is syntaxly correct [testenv:bandit] -basepython=python3.6 +basepython=python2.7 skipsdist=True deps=bandit commands=bandit -b bandit.json -r oauthlib/ From bbbcca731d5db16d7b1765070880aa54288788e9 Mon Sep 17 00:00:00 2001 From: Abhishek Patel <5524161+Abhishek8394@users.noreply.github.com> Date: Mon, 6 May 2019 22:28:23 -0700 Subject: [PATCH 210/237] Add validation check for presence of forbidden query parameters in OAuth2 TokenEndpoint, IntrospectionEndpoint and RevocationEndpoint --- oauthlib/oauth2/rfc6749/endpoints/base.py | 12 ++++++++++++ oauthlib/oauth2/rfc6749/endpoints/introspect.py | 1 + oauthlib/oauth2/rfc6749/endpoints/revocation.py | 1 + oauthlib/oauth2/rfc6749/endpoints/token.py | 5 ++++- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/base.py b/oauthlib/oauth2/rfc6749/endpoints/base.py index c0fc7269..29086e46 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/base.py +++ b/oauthlib/oauth2/rfc6749/endpoints/base.py @@ -15,6 +15,8 @@ TemporarilyUnavailableError, InvalidRequestError, InvalidClientError, UnsupportedTokenTypeError) +from oauthlib.common import CaseInsensitiveDict + log = logging.getLogger(__name__) @@ -23,6 +25,7 @@ class BaseEndpoint(object): def __init__(self): self._available = True self._catch_errors = False + self._blacklist_query_params = {'client_secret', 'code_verifier'} @property def available(self): @@ -62,6 +65,15 @@ def _raise_on_unsupported_token(self, request): request.token_type_hint not in self.supported_token_types): raise UnsupportedTokenTypeError(request=request) + def _raise_on_bad_post_request(self, request): + """Raise if invalid POST request received + """ + if request.http_method.lower() == 'post': + query_params = CaseInsensitiveDict(urldecode(request.uri_query)) + for k in self._blacklist_query_params: + if k in query_params: + raise InvalidRequestError(request=request, + description='Query parameters 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 47022fd0..547e7dbd 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/introspect.py +++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py @@ -117,6 +117,7 @@ def validate_introspect_request(self, request): .. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5 .. _`RFC6749`: http://tools.ietf.org/html/rfc6749 """ + 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 fda3f30e..14394911 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py +++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py @@ -121,6 +121,7 @@ def validate_revocation_request(self, request): .. _`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_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 90fb16f4..223e8d08 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/token.py +++ b/oauthlib/oauth2/rfc6749/endpoints/token.py @@ -91,7 +91,7 @@ def create_token_response(self, uri, http_method='GET', body=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 +115,6 @@ def create_token_response(self, uri, http_method='GET', body=None, 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_post_request(request) From 047ceccf48ea7ccd4ecc6b48a8ddb6dd4a14abd6 Mon Sep 17 00:00:00 2001 From: Abhishek Patel <5524161+Abhishek8394@users.noreply.github.com> Date: Mon, 6 May 2019 23:26:29 -0700 Subject: [PATCH 211/237] Add tests + create a global variable for blacklisted query parameters --- oauthlib/oauth2/rfc6749/endpoints/base.py | 16 ++++++----- .../rfc6749/endpoints/test_error_responses.py | 27 +++++++++++++++++++ .../endpoints/test_introspect_endpoint.py | 16 +++++++++++ .../endpoints/test_revocation_endpoint.py | 16 +++++++++++ 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/base.py b/oauthlib/oauth2/rfc6749/endpoints/base.py index 29086e46..dc3204b2 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/base.py +++ b/oauthlib/oauth2/rfc6749/endpoints/base.py @@ -15,17 +15,18 @@ TemporarilyUnavailableError, InvalidRequestError, InvalidClientError, UnsupportedTokenTypeError) -from oauthlib.common import CaseInsensitiveDict +from oauthlib.common import CaseInsensitiveDict, urldecode log = logging.getLogger(__name__) +BLACKLIST_QUERY_PARAMS = {'client_secret', 'code_verifier'} class BaseEndpoint(object): def __init__(self): self._available = True self._catch_errors = False - self._blacklist_query_params = {'client_secret', 'code_verifier'} + self._blacklist_query_params = BLACKLIST_QUERY_PARAMS @property def available(self): @@ -33,7 +34,7 @@ def available(self): @available.setter def available(self, available): - self._available = available + self._available = available @property def catch_errors(self): @@ -69,11 +70,12 @@ def _raise_on_bad_post_request(self, request): """Raise if invalid POST request received """ if request.http_method.lower() == 'post': - query_params = CaseInsensitiveDict(urldecode(request.uri_query)) - for k in self._blacklist_query_params: - if k in query_params: + query_params = CaseInsensitiveDict(dict(urldecode(request.uri_query))) + for param in self._blacklist_query_params: + if param in query_params: raise InvalidRequestError(request=request, - description='Query parameters not allowed') + description=('"%s" is not allowed as a url query' +\ + ' parameter') % (param)) def catch_errors_and_unavailability(f): @functools.wraps(f) diff --git a/tests/oauth2/rfc6749/endpoints/test_error_responses.py b/tests/oauth2/rfc6749/endpoints/test_error_responses.py index a249cb1c..4a288ad4 100644 --- a/tests/oauth2/rfc6749/endpoints/test_error_responses.py +++ b/tests/oauth2/rfc6749/endpoints/test_error_responses.py @@ -6,10 +6,12 @@ import mock +from oauthlib.common import urlencode from oauthlib.oauth2 import (BackendApplicationServer, LegacyApplicationServer, MobileApplicationServer, RequestValidator, WebApplicationServer) from oauthlib.oauth2.rfc6749 import errors +from oauthlib.oauth2.rfc6749.endpoints.base import BLACKLIST_QUERY_PARAMS from ....unittest import TestCase @@ -437,3 +439,28 @@ def test_unsupported_grant_type(self): _, 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_post_request(self): + self.validator.authenticate_client.side_effect = self.set_client + for param in BLACKLIST_QUERY_PARAMS: + uri = 'https://i/b/token?' + urlencode([(param, 'secret')]) + _, body, s = self.web.create_introspect_response(uri, + body='grant_type=access_token&code=123') + self.assertEqual(json.loads(body)['error'], 'invalid_request') + self.assertIn(param, json.loads(body)['error_description']) + self.assertIn('not allowed', json.loads(body)['error_description']) + self.assertEqual(s, 400) + + _, body, s = self.legacy.create_introspect_response(uri, + body='grant_type=access_token&code=123') + self.assertEqual(json.loads(body)['error'], 'invalid_request') + self.assertIn(param, json.loads(body)['error_description']) + self.assertIn('not allowed', json.loads(body)['error_description']) + self.assertEqual(s, 400) + + _, body, s = self.backend.create_introspect_response(uri, + body='grant_type=access_token&code=123') + self.assertEqual(json.loads(body)['error'], 'invalid_request') + self.assertIn(param, json.loads(body)['error_description']) + self.assertIn('not allowed', json.loads(body)['error_description']) + self.assertEqual(s, 400) diff --git a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py index b9bf76a5..234a4efb 100644 --- a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py @@ -7,6 +7,7 @@ from oauthlib.common import urlencode from oauthlib.oauth2 import RequestValidator, IntrospectEndpoint +from oauthlib.oauth2.rfc6749.endpoints.base import BLACKLIST_QUERY_PARAMS from ....unittest import TestCase @@ -139,3 +140,18 @@ def test_introspect_unsupported_token(self): self.assertEqual(h, self.resp_h) self.assertEqual(loads(b)['error'], 'invalid_request') self.assertEqual(s, 400) + + def test_introspect_bad_post_request(self): + endpoint = IntrospectEndpoint(self.validator, + supported_token_types=['access_token']) + for param in BLACKLIST_QUERY_PARAMS: + 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(param, loads(b)['error_description']) + self.assertIn('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 2a241770..e89c3bd0 100644 --- a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py @@ -7,6 +7,7 @@ from oauthlib.common import urlencode from oauthlib.oauth2 import RequestValidator, RevocationEndpoint +from oauthlib.oauth2.rfc6749.endpoints.base import BLACKLIST_QUERY_PARAMS from ....unittest import TestCase @@ -120,3 +121,18 @@ def test_revoke_unsupported_token(self): self.assertEqual(h, self.resp_h) self.assertEqual(loads(b)['error'], 'invalid_request') self.assertEqual(s, 400) + + def test_revoke_bad_post_request(self): + endpoint = RevocationEndpoint(self.validator, + supported_token_types=['access_token']) + for param in BLACKLIST_QUERY_PARAMS: + 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(param, loads(b)['error_description']) + self.assertIn('not allowed', loads(b)['error_description']) + self.assertEqual(s, 400) From ee06f0f3349d7fd656d35a2eef40ee18fb74e303 Mon Sep 17 00:00:00 2001 From: Abhishek Patel <5524161+Abhishek8394@users.noreply.github.com> Date: Sun, 12 May 2019 20:35:00 -0700 Subject: [PATCH 212/237] Ban all query parameters on Intropspection, Token and Revocation endpopoint --- oauthlib/oauth2/rfc6749/endpoints/base.py | 12 ++++-------- .../oauth2/rfc6749/endpoints/test_error_responses.py | 12 ++++-------- .../rfc6749/endpoints/test_introspect_endpoint.py | 11 +++++------ .../rfc6749/endpoints/test_revocation_endpoint.py | 6 ++---- 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/base.py b/oauthlib/oauth2/rfc6749/endpoints/base.py index dc3204b2..c99c22dd 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/base.py +++ b/oauthlib/oauth2/rfc6749/endpoints/base.py @@ -19,14 +19,12 @@ log = logging.getLogger(__name__) -BLACKLIST_QUERY_PARAMS = {'client_secret', 'code_verifier'} class BaseEndpoint(object): def __init__(self): self._available = True self._catch_errors = False - self._blacklist_query_params = BLACKLIST_QUERY_PARAMS @property def available(self): @@ -70,12 +68,10 @@ def _raise_on_bad_post_request(self, request): """Raise if invalid POST request received """ if request.http_method.lower() == 'post': - query_params = CaseInsensitiveDict(dict(urldecode(request.uri_query))) - for param in self._blacklist_query_params: - if param in query_params: - raise InvalidRequestError(request=request, - description=('"%s" is not allowed as a url query' +\ - ' parameter') % (param)) + 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/tests/oauth2/rfc6749/endpoints/test_error_responses.py b/tests/oauth2/rfc6749/endpoints/test_error_responses.py index 4a288ad4..2b870322 100644 --- a/tests/oauth2/rfc6749/endpoints/test_error_responses.py +++ b/tests/oauth2/rfc6749/endpoints/test_error_responses.py @@ -11,7 +11,6 @@ MobileApplicationServer, RequestValidator, WebApplicationServer) from oauthlib.oauth2.rfc6749 import errors -from oauthlib.oauth2.rfc6749.endpoints.base import BLACKLIST_QUERY_PARAMS from ....unittest import TestCase @@ -442,25 +441,22 @@ def test_unsupported_grant_type(self): def test_invalid_post_request(self): self.validator.authenticate_client.side_effect = self.set_client - for param in BLACKLIST_QUERY_PARAMS: + for param in ['token', 'secret', 'code', 'foo']: uri = 'https://i/b/token?' + urlencode([(param, 'secret')]) _, body, s = self.web.create_introspect_response(uri, body='grant_type=access_token&code=123') self.assertEqual(json.loads(body)['error'], 'invalid_request') - self.assertIn(param, json.loads(body)['error_description']) - self.assertIn('not allowed', json.loads(body)['error_description']) + self.assertIn('query parameters are not allowed', json.loads(body)['error_description']) self.assertEqual(s, 400) _, body, s = self.legacy.create_introspect_response(uri, body='grant_type=access_token&code=123') self.assertEqual(json.loads(body)['error'], 'invalid_request') - self.assertIn(param, json.loads(body)['error_description']) - self.assertIn('not allowed', json.loads(body)['error_description']) + self.assertIn('query parameters are not allowed', json.loads(body)['error_description']) self.assertEqual(s, 400) _, body, s = self.backend.create_introspect_response(uri, body='grant_type=access_token&code=123') self.assertEqual(json.loads(body)['error'], 'invalid_request') - self.assertIn(param, json.loads(body)['error_description']) - self.assertIn('not allowed', json.loads(body)['error_description']) + self.assertIn('query parameters are not allowed', json.loads(body)['error_description']) self.assertEqual(s, 400) diff --git a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py index 234a4efb..a34c970e 100644 --- a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py @@ -7,7 +7,6 @@ from oauthlib.common import urlencode from oauthlib.oauth2 import RequestValidator, IntrospectEndpoint -from oauthlib.oauth2.rfc6749.endpoints.base import BLACKLIST_QUERY_PARAMS from ....unittest import TestCase @@ -144,14 +143,14 @@ def test_introspect_unsupported_token(self): def test_introspect_bad_post_request(self): endpoint = IntrospectEndpoint(self.validator, supported_token_types=['access_token']) - for param in BLACKLIST_QUERY_PARAMS: + 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) + 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(param, loads(b)['error_description']) - self.assertIn('not allowed', loads(b)['error_description']) + 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 e89c3bd0..c73a1ef0 100644 --- a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py @@ -7,7 +7,6 @@ from oauthlib.common import urlencode from oauthlib.oauth2 import RequestValidator, RevocationEndpoint -from oauthlib.oauth2.rfc6749.endpoints.base import BLACKLIST_QUERY_PARAMS from ....unittest import TestCase @@ -125,7 +124,7 @@ def test_revoke_unsupported_token(self): def test_revoke_bad_post_request(self): endpoint = RevocationEndpoint(self.validator, supported_token_types=['access_token']) - for param in BLACKLIST_QUERY_PARAMS: + for param in ['token', 'secret', 'code', 'foo']: uri = 'http://some.endpoint?' + urlencode([(param, 'secret')]) body = urlencode([('token', 'foo'), ('token_type_hint', 'access_token')]) @@ -133,6 +132,5 @@ def test_revoke_bad_post_request(self): headers=self.headers, body=body) self.assertEqual(h, self.resp_h) self.assertEqual(loads(b)['error'], 'invalid_request') - self.assertIn(param, loads(b)['error_description']) - self.assertIn('not allowed', loads(b)['error_description']) + self.assertIn('query parameters are not allowed', loads(b)['error_description']) self.assertEqual(s, 400) From 056948ac7c14a435d0b65dd27692fe2494bc3743 Mon Sep 17 00:00:00 2001 From: Abhishek Patel <5524161+Abhishek8394@users.noreply.github.com> Date: Tue, 14 May 2019 00:36:10 -0700 Subject: [PATCH 213/237] Enforce POST HTTP method on TokenEndpoint, IntrospectEndpoint and RevocationEndpoint - Add validation checks for HTTP method in TokenEndpoint, IntrospectEndpoint and RevocationEndpoint. - CHANGE DEFAULT HTTP method for TokenEndpoint from 'GET' to 'POST'. - Add tests + Fix an old test in . It used to send query params to TokenEndpoint which is not allowed anymore. Fixed it so payload is sent as POST body. --- oauthlib/oauth2/rfc6749/endpoints/base.py | 21 +++++- .../oauth2/rfc6749/endpoints/introspect.py | 2 + .../oauth2/rfc6749/endpoints/revocation.py | 2 + oauthlib/oauth2/rfc6749/endpoints/token.py | 5 +- .../rfc6749/endpoints/test_base_endpoint.py | 2 +- .../rfc6749/endpoints/test_error_responses.py | 66 ++++++++++++++----- .../endpoints/test_introspect_endpoint.py | 15 +++++ .../endpoints/test_revocation_endpoint.py | 15 +++++ 8 files changed, 107 insertions(+), 21 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/endpoints/base.py b/oauthlib/oauth2/rfc6749/endpoints/base.py index c99c22dd..e39232f4 100644 --- a/oauthlib/oauth2/rfc6749/endpoints/base.py +++ b/oauthlib/oauth2/rfc6749/endpoints/base.py @@ -25,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): @@ -64,10 +76,17 @@ def _raise_on_unsupported_token(self, request): 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.lower() == 'post': + if request.http_method.upper() == 'POST': query_params = request.uri_query or "" if query_params: raise InvalidRequestError(request=request, diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py index 547e7dbd..4accbdc1 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,7 @@ def validate_introspect_request(self, request): .. _`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) diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py index 14394911..1fabd038 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,7 @@ def validate_revocation_request(self, request): .. _`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) diff --git a/oauthlib/oauth2/rfc6749/endpoints/token.py b/oauthlib/oauth2/rfc6749/endpoints/token.py index 223e8d08..bc87e9bd 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,7 +87,7 @@ def default_token_type(self): 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.""" @@ -117,4 +119,5 @@ def create_token_response(self, uri, http_method='GET', body=None, 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/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py index 4f78d9b3..bf04a42b 100644 --- a/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py @@ -25,7 +25,7 @@ def test_error_catching(self): 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 2b870322..24798367 100644 --- a/tests/oauth2/rfc6749/endpoints/test_error_responses.py +++ b/tests/oauth2/rfc6749/endpoints/test_error_responses.py @@ -11,7 +11,6 @@ MobileApplicationServer, RequestValidator, WebApplicationServer) from oauthlib.oauth2.rfc6749 import errors - from ....unittest import TestCase @@ -439,24 +438,55 @@ def test_unsupported_grant_type(self): 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')]) - _, body, s = self.web.create_introspect_response(uri, - body='grant_type=access_token&code=123') - self.assertEqual(json.loads(body)['error'], 'invalid_request') - self.assertIn('query parameters are not allowed', json.loads(body)['error_description']) - self.assertEqual(s, 400) - - _, body, s = self.legacy.create_introspect_response(uri, - body='grant_type=access_token&code=123') - self.assertEqual(json.loads(body)['error'], 'invalid_request') - self.assertIn('query parameters are not allowed', json.loads(body)['error_description']) - self.assertEqual(s, 400) - - _, body, s = self.backend.create_introspect_response(uri, - body='grant_type=access_token&code=123') - self.assertEqual(json.loads(body)['error'], 'invalid_request') - self.assertIn('query parameters are not allowed', json.loads(body)['error_description']) - self.assertEqual(s, 400) + 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 a34c970e..ae3deaef 100644 --- a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py @@ -140,6 +140,21 @@ def test_introspect_unsupported_token(self): 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']) diff --git a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py index c73a1ef0..17be3a5a 100644 --- a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py +++ b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py @@ -121,6 +121,21 @@ def test_revoke_unsupported_token(self): 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']) From 16a5e630a1d1eeb2511e1049ee2291b33f57ab25 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 13 May 2019 14:53:41 +0200 Subject: [PATCH 214/237] Updated bandit baseline after review --- bandit.json | 1184 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1179 insertions(+), 5 deletions(-) diff --git a/bandit.json b/bandit.json index 02e15a83..4d3bfe19 100644 --- a/bandit.json +++ b/bandit.json @@ -1,21 +1,1034 @@ { "errors": [], - "generated_at": "2018-12-13T10:39:37Z", + "generated_at": "2019-05-13T12:51:49Z", + "metrics": { + "_totals": { + "CONFIDENCE.HIGH": 3.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 10.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 12.0, + "SEVERITY.MEDIUM": 1.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 8338, + "nosec": 0 + }, + "oauthlib/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 25, + "nosec": 0 + }, + "oauthlib/common.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 337, + "nosec": 0 + }, + "oauthlib/oauth1/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 16, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/__init__.py": { + "CONFIDENCE.HIGH": 1.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 1.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 230, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 8, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/access_token.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 152, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/authorization.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 135, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/base.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 142, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/pre_configured.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 10, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/request_token.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 141, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/resource.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 97, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/endpoints/signature_only.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 53, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/errors.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 58, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/parameters.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 75, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/request_validator.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 630, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/signature.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 379, + "nosec": 0 + }, + "oauthlib/oauth1/rfc5849/utils.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 61, + "nosec": 0 + }, + "oauthlib/oauth2/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 33, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 14, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/clients/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 13, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/clients/backend_application.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 56, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/clients/base.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 3.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 3.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 384, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/clients/legacy_application.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 67, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/clients/mobile_application.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 140, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/clients/service_application.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 144, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/clients/web_application.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 165, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 18, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/authorization.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 85, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/base.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 71, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/introspect.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 98, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/metadata.py": { + "CONFIDENCE.HIGH": 2.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 2.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 182, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 5.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 5.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 189, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/resource.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 65, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/revocation.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 96, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/endpoints/token.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 76, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/errors.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 311, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/grant_types/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 10, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/grant_types/authorization_code.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 389, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/grant_types/base.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 199, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/grant_types/client_credentials.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 96, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/grant_types/implicit.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 259, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/grant_types/refresh_token.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 102, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 156, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/parameters.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 1.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 1.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 335, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/request_validator.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 504, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/tokens.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 277, + "nosec": 0 + }, + "oauthlib/oauth2/rfc6749/utils.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 70, + "nosec": 0 + }, + "oauthlib/openid/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 8, + "nosec": 0 + }, + "oauthlib/openid/connect/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 0, + "nosec": 0 + }, + "oauthlib/openid/connect/core/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 0, + "nosec": 0 + }, + "oauthlib/openid/connect/core/endpoints/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 9, + "nosec": 0 + }, + "oauthlib/openid/connect/core/endpoints/pre_configured.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 1.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 1.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 93, + "nosec": 0 + }, + "oauthlib/openid/connect/core/endpoints/userinfo.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 83, + "nosec": 0 + }, + "oauthlib/openid/connect/core/exceptions.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 117, + "nosec": 0 + }, + "oauthlib/openid/connect/core/grant_types/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 15, + "nosec": 0 + }, + "oauthlib/openid/connect/core/grant_types/authorization_code.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 32, + "nosec": 0 + }, + "oauthlib/openid/connect/core/grant_types/base.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 234, + "nosec": 0 + }, + "oauthlib/openid/connect/core/grant_types/dispatchers.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 66, + "nosec": 0 + }, + "oauthlib/openid/connect/core/grant_types/exceptions.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 26, + "nosec": 0 + }, + "oauthlib/openid/connect/core/grant_types/hybrid.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 38, + "nosec": 0 + }, + "oauthlib/openid/connect/core/grant_types/implicit.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 35, + "nosec": 0 + }, + "oauthlib/openid/connect/core/request_validator.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 235, + "nosec": 0 + }, + "oauthlib/openid/connect/core/tokens.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 42, + "nosec": 0 + }, + "oauthlib/signals.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 32, + "nosec": 0 + }, + "oauthlib/tokens/__init__.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 0, + "nosec": 0 + }, + "oauthlib/tokens/access_token.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 13, + "nosec": 0 + }, + "oauthlib/tokens/base.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 8, + "nosec": 0 + }, + "oauthlib/tokens/id_token.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 6, + "nosec": 0 + }, + "oauthlib/uri_validate.py": { + "CONFIDENCE.HIGH": 0.0, + "CONFIDENCE.LOW": 0.0, + "CONFIDENCE.MEDIUM": 0.0, + "CONFIDENCE.UNDEFINED": 0.0, + "SEVERITY.HIGH": 0.0, + "SEVERITY.LOW": 0.0, + "SEVERITY.MEDIUM": 0.0, + "SEVERITY.UNDEFINED": 0.0, + "loc": 93, + "nosec": 0 + } + }, "results": [ { - "code": "182 if request.body is not None and content_type_eligible:\n183 params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8')))\n184 \n", + "code": "183 if request.body is not None and content_type_eligible:\n184 params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8')))\n185 \n", "filename": "oauthlib/oauth1/rfc5849/__init__.py", "issue_confidence": "HIGH", "issue_severity": "MEDIUM", "issue_text": "Use of insecure MD2, MD4, MD5, or SHA1 hash function.", - "line_number": 183, + "line_number": 184, "line_range": [ - 183 + 184 ], "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html#b303-md5", "test_id": "B303", "test_name": "blacklist" }, + { + "code": "49 \"\"\"\n50 refresh_token_key = 'refresh_token'\n51 \n52 def __init__(self, client_id,\n", + "filename": "oauthlib/oauth2/rfc6749/clients/base.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'refresh_token'", + "line_number": 50, + "line_range": [ + 50, + 51 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b105_hardcoded_password_string.html", + "test_id": "B105", + "test_name": "hardcoded_password_string" + }, + { + "code": "51 \n52 def __init__(self, client_id,\n53 default_token_placement=AUTH_HEADER,\n54 token_type='Bearer',\n55 access_token=None,\n56 refresh_token=None,\n57 mac_key=None,\n58 mac_algorithm=None,\n59 token=None,\n60 scope=None,\n61 state=None,\n62 redirect_url=None,\n63 state_generator=generate_token,\n64 **kwargs):\n65 \"\"\"Initialize a client with commonly used attributes.\n66 \n67 :param client_id: Client identifier given by the OAuth provider upon\n68 registration.\n69 \n70 :param default_token_placement: Tokens can be supplied in the Authorization\n71 header (default), the URL query component (``query``) or the request\n72 body (``body``).\n73 \n74 :param token_type: OAuth 2 token type. Defaults to Bearer. Change this\n75 if you specify the ``access_token`` parameter and know it is of a\n76 different token type, such as a MAC, JWT or SAML token. Can\n77 also be supplied as ``token_type`` inside the ``token`` dict parameter.\n78 \n79 :param access_token: An access token (string) used to authenticate\n80 requests to protected resources. Can also be supplied inside the\n81 ``token`` dict parameter.\n82 \n83 :param refresh_token: A refresh token (string) used to refresh expired\n84 tokens. Can also be supplied inside the ``token`` dict parameter.\n85 \n86 :param mac_key: Encryption key used with MAC tokens.\n87 \n88 :param mac_algorithm: Hashing algorithm for MAC tokens.\n89 \n90 :param token: A dict of token attributes such as ``access_token``,\n91 ``token_type`` and ``expires_at``.\n92 \n93 :param scope: A list of default scopes to request authorization for.\n94 \n95 :param state: A CSRF protection string used during authorization.\n96 \n97 :param redirect_url: The redirection endpoint on the client side to which\n98 the user returns after authorization.\n99 \n100 :param state_generator: A no argument state generation callable. Defaults\n101 to :py:meth:`oauthlib.common.generate_token`.\n102 \"\"\"\n103 \n104 self.client_id = client_id\n105 self.default_token_placement = default_token_placement\n106 self.token_type = token_type\n107 self.access_token = access_token\n108 self.refresh_token = refresh_token\n109 self.mac_key = mac_key\n110 self.mac_algorithm = mac_algorithm\n111 self.token = token or {}\n112 self.scope = scope\n113 self.state_generator = state_generator\n114 self.state = state\n115 self.redirect_url = redirect_url\n116 self.code = None\n117 self.expires_in = None\n118 self._expires_at = None\n119 self.populate_token_attributes(self.token)\n120 \n121 @property\n", + "filename": "oauthlib/oauth2/rfc6749/clients/base.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'Bearer'", + "line_number": 52, + "line_range": [ + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html", + "test_id": "B107", + "test_name": "hardcoded_password_default" + }, + { + "code": "313 \n314 def prepare_token_revocation_request(self, revocation_url, token,\n315 token_type_hint=\"access_token\", body='', callback=None, **kwargs):\n316 \"\"\"Prepare a token revocation request.\n317 \n318 :param revocation_url: Provider token revocation endpoint URL.\n319 \n320 :param token: The access or refresh token to be revoked (string).\n321 \n322 :param token_type_hint: ``\"access_token\"`` (default) or\n323 ``\"refresh_token\"``. This is optional and if you wish to not pass it you\n324 must provide ``token_type_hint=None``.\n325 \n326 :param body:\n327 \n328 :param callback: A jsonp callback such as ``package.callback`` to be invoked\n329 upon receiving the response. Not that it should not include a () suffix.\n330 \n331 :param kwargs: Additional parameters to included in the request.\n332 \n333 :returns: The prepared request tuple with (url, headers, body).\n334 \n335 Note that JSONP request may use GET requests as the parameters will\n336 be added to the request URL query as opposed to the request body.\n337 \n338 An example of a revocation request\n339 \n340 .. code-block: http\n341 \n342 POST /revoke HTTP/1.1\n343 Host: server.example.com\n344 Content-Type: application/x-www-form-urlencoded\n345 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW\n346 \n347 token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token\n348 \n349 An example of a jsonp revocation request\n350 \n351 .. code-block: http\n352 \n353 GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1\n354 Host: server.example.com\n355 Content-Type: application/x-www-form-urlencoded\n356 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW\n357 \n358 and an error response\n359 \n360 .. code-block: http\n361 \n362 package.myCallback({\"error\":\"unsupported_token_type\"});\n363 \n364 Note that these requests usually require client credentials, client_id in\n365 the case for public clients and provider specific authentication\n366 credentials for confidential clients.\n367 \"\"\"\n368 if not is_secure_transport(revocation_url):\n369 raise InsecureTransportError()\n370 \n371 return prepare_token_revocation_request(revocation_url, token,\n372 token_type_hint=token_type_hint, body=body, callback=callback,\n373 **kwargs)\n374 \n375 def parse_request_body_response(self, body, scope=None, **kwargs):\n", + "filename": "oauthlib/oauth2/rfc6749/clients/base.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'access_token'", + "line_number": 314, + "line_range": [ + 314, + 315, + 316, + 317, + 318, + 319, + 320, + 321, + 322, + 323, + 324, + 325, + 326, + 327, + 328, + 329, + 330, + 331, + 332, + 333, + 334, + 335, + 336, + 337, + 338, + 339, + 340, + 341, + 342, + 343, + 344, + 345, + 346, + 347, + 348, + 349, + 350, + 351, + 352, + 353, + 354, + 355, + 356, + 357, + 358, + 359, + 360, + 361, + 362, + 363, + 364, + 365, + 366, + 367, + 368, + 369, + 370, + 371, + 372, + 373, + 374 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html", + "test_id": "B107", + "test_name": "hardcoded_password_default" + }, { "code": "45 def __init__(self, endpoints, claims={}, raise_errors=True):\n46 assert isinstance(claims, dict)\n47 for endpoint in endpoints:\n", "filename": "oauthlib/oauth2/rfc6749/endpoints/metadata.py", @@ -43,6 +1056,167 @@ "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b101_assert_used.html", "test_id": "B101", "test_name": "assert_used" + }, + { + "code": "70 default_token_type=bearer)\n71 ResourceEndpoint.__init__(self, default_token='Bearer',\n72 token_types={'Bearer': bearer})\n73 RevocationEndpoint.__init__(self, request_validator)\n", + "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'Bearer'", + "line_number": 71, + "line_range": [ + 71, + 72 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", + "test_id": "B106", + "test_name": "hardcoded_password_funcarg" + }, + { + "code": "109 default_token_type=bearer)\n110 ResourceEndpoint.__init__(self, default_token='Bearer',\n111 token_types={'Bearer': bearer})\n112 RevocationEndpoint.__init__(self, request_validator)\n", + "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'Bearer'", + "line_number": 110, + "line_range": [ + 110, + 111 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", + "test_id": "B106", + "test_name": "hardcoded_password_funcarg" + }, + { + "code": "142 default_token_type=bearer)\n143 ResourceEndpoint.__init__(self, default_token='Bearer',\n144 token_types={'Bearer': bearer})\n145 RevocationEndpoint.__init__(self, request_validator,\n", + "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'Bearer'", + "line_number": 143, + "line_range": [ + 143, + 144 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", + "test_id": "B106", + "test_name": "hardcoded_password_funcarg" + }, + { + "code": "181 default_token_type=bearer)\n182 ResourceEndpoint.__init__(self, default_token='Bearer',\n183 token_types={'Bearer': bearer})\n184 RevocationEndpoint.__init__(self, request_validator)\n", + "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'Bearer'", + "line_number": 182, + "line_range": [ + 182, + 183 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", + "test_id": "B106", + "test_name": "hardcoded_password_funcarg" + }, + { + "code": "214 default_token_type=bearer)\n215 ResourceEndpoint.__init__(self, default_token='Bearer',\n216 token_types={'Bearer': bearer})\n217 RevocationEndpoint.__init__(self, request_validator,\n", + "filename": "oauthlib/oauth2/rfc6749/endpoints/pre_configured.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'Bearer'", + "line_number": 215, + "line_range": [ + 215, + 216 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", + "test_id": "B106", + "test_name": "hardcoded_password_funcarg" + }, + { + "code": "164 \n165 def prepare_token_revocation_request(url, token, token_type_hint=\"access_token\",\n166 callback=None, body='', **kwargs):\n167 \"\"\"Prepare a token revocation request.\n168 \n169 The client constructs the request by including the following parameters\n170 using the \"application/x-www-form-urlencoded\" format in the HTTP request\n171 entity-body:\n172 \n173 :param token: REQUIRED. The token that the client wants to get revoked.\n174 \n175 :param token_type_hint: OPTIONAL. A hint about the type of the token\n176 submitted for revocation. Clients MAY pass this\n177 parameter in order to help the authorization server\n178 to optimize the token lookup. If the server is\n179 unable to locate the token using the given hint, it\n180 MUST extend its search across all of its supported\n181 token types. An authorization server MAY ignore\n182 this parameter, particularly if it is able to detect\n183 the token type automatically.\n184 \n185 This specification defines two values for `token_type_hint`:\n186 \n187 * access_token: An access token as defined in [RFC6749],\n188 `Section 1.4`_\n189 \n190 * refresh_token: A refresh token as defined in [RFC6749],\n191 `Section 1.5`_\n192 \n193 Specific implementations, profiles, and extensions of this\n194 specification MAY define other values for this parameter using the\n195 registry defined in `Section 4.1.2`_.\n196 \n197 .. _`Section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4\n198 .. _`Section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5\n199 .. _`Section 4.1.2`: https://tools.ietf.org/html/rfc7009#section-4.1.2\n200 \n201 \"\"\"\n202 if not is_secure_transport(url):\n203 raise InsecureTransportError()\n204 \n205 params = [('token', token)]\n206 \n207 if token_type_hint:\n208 params.append(('token_type_hint', token_type_hint))\n209 \n210 for k in kwargs:\n211 if kwargs[k]:\n212 params.append((unicode_type(k), kwargs[k]))\n213 \n214 headers = {'Content-Type': 'application/x-www-form-urlencoded'}\n215 \n216 if callback:\n217 params.append(('callback', callback))\n218 return add_params_to_uri(url, params), headers, body\n219 else:\n220 return url, headers, add_params_to_qs(body, params)\n221 \n222 \n223 def parse_authorization_code_response(uri, state=None):\n", + "filename": "oauthlib/oauth2/rfc6749/parameters.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'access_token'", + "line_number": 165, + "line_range": [ + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 174, + 175, + 176, + 177, + 178, + 179, + 180, + 181, + 182, + 183, + 184, + 185, + 186, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 209, + 210, + 211, + 212, + 213, + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b107_hardcoded_password_default.html", + "test_id": "B107", + "test_name": "hardcoded_password_default" + }, + { + "code": "104 default_token_type=bearer)\n105 ResourceEndpoint.__init__(self, default_token='Bearer',\n106 token_types={'Bearer': bearer, 'JWT': jwt})\n107 RevocationEndpoint.__init__(self, request_validator)\n", + "filename": "oauthlib/openid/connect/core/endpoints/pre_configured.py", + "issue_confidence": "MEDIUM", + "issue_severity": "LOW", + "issue_text": "Possible hardcoded password: 'Bearer'", + "line_number": 105, + "line_range": [ + 105, + 106 + ], + "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html", + "test_id": "B106", + "test_name": "hardcoded_password_funcarg" } ] -} +} \ No newline at end of file From f09f687ebf02cb05ffdf3378e8207fb0083b3049 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 13 May 2019 15:08:18 +0200 Subject: [PATCH 215/237] Downgrade python to match with Travis --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 1cac71c1..9cd2a9fb 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,7 @@ commands= echo setup.py/long description is syntaxly correct [testenv:bandit] +basepython=python2.7 skipsdist=True deps=bandit commands=bandit -b bandit.json -r oauthlib/ From 09bcb01032a21a4bfa0c478ea8ae66ec8ace957a Mon Sep 17 00:00:00 2001 From: Mark Gregson Date: Thu, 6 Jun 2019 14:08:18 +1000 Subject: [PATCH 216/237] Check for authorization response errors --- oauthlib/oauth2/rfc6749/parameters.py | 9 ++++++--- tests/oauth2/rfc6749/test_parameters.py | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 6b9d6306..df724eec 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 diff --git a/tests/oauth2/rfc6749/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py index c42f516c..0d293cc5 100644 --- a/tests/oauth2/rfc6749/test_parameters.py +++ b/tests/oauth2/rfc6749/test_parameters.py @@ -73,7 +73,8 @@ def setUp(self): 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) @@ -180,8 +181,10 @@ def test_grant_response(self): 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, From d2dcb0f5bb247c9e48fa876e3c99ff3298b3a4c0 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 11 Jun 2019 11:45:06 +0300 Subject: [PATCH 217/237] Create FUNDING.yml --- .github/FUNDING.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..9d4faec3 --- /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 From 34166c5182871b53b2761dd8fae00aaec611a7d3 Mon Sep 17 00:00:00 2001 From: qporest Date: Tue, 2 Jul 2019 15:31:51 -0400 Subject: [PATCH 218/237] Fix BackendApplicationClient.prepare_request_body Currently, if no `scope` is passed to `prepare_request_body`, None will be passed on to `prepare_token_request`, even if BackendApplicationClient was initialized with `scope`. --- oauthlib/oauth2/rfc6749/clients/backend_application.py | 1 + 1 file changed, 1 insertion(+) diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py index 2483e56e..57378140 100644 --- a/oauthlib/oauth2/rfc6749/clients/backend_application.py +++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py @@ -71,5 +71,6 @@ def prepare_request_body(self, body='', scope=None, """ 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) From 19d111df1c55456c0c85b6ba8051d3a9b3ac3733 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 3 Jul 2019 18:28:34 +0200 Subject: [PATCH 219/237] Error in timestamp comparison --- tests/openid/connect/core/grant_types/test_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/openid/connect/core/grant_types/test_base.py b/tests/openid/connect/core/grant_types/test_base.py index 76e017f9..d506b7ed 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 @@ def test_finalize_id_token(self): 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") From cb6af4b44da264613250cb3d99be420dbeb8e268 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Wed, 20 Feb 2019 14:30:03 +0100 Subject: [PATCH 220/237] Fix 652: removed "state" from /token response. Fix OIDC /token flow where &state=None was always returned, and fix OAuth2.0 /token flow where &state=foobar was returned if &state=foobar was present in the token request. Remove "save_token" from create_token() signature cuz it was not used internally. Deprecated the option to let upstream libraries have a chance to remove it, if ever used. --- CHANGELOG.rst | 8 ++++ .../rfc6749/grant_types/authorization_code.py | 4 +- .../rfc6749/grant_types/client_credentials.py | 3 +- .../oauth2/rfc6749/grant_types/implicit.py | 5 ++- .../rfc6749/grant_types/refresh_token.py | 3 +- .../resource_owner_password_credentials.py | 3 +- oauthlib/oauth2/rfc6749/tokens.py | 18 ++++----- .../openid/connect/core/grant_types/base.py | 3 -- .../connect/core/grant_types/implicit.py | 5 +++ oauthlib/openid/connect/core/tokens.py | 2 +- .../test_credentials_preservation.py | 12 ------ tests/oauth2/rfc6749/test_server.py | 39 +++++++++++++------ tests/openid/connect/core/test_server.py | 16 +++++--- 13 files changed, 72 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2cc0dd37..9e0efdaf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Changelog ========= +TBD +------------------ +* #652: Fixed OIDC /token response which wrongly returned "&state=None" + +3.0.1 (2019-01-24) +------------------ +* Fixed OAuth2.0 regression introduced in 3.0.0: Revocation with Basic auth no longer possible #644 + 3.0.0 (2019-01-01) ------------------ OAuth2.0 Provider - outstanding Features diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py index 64633917..5f03d9cb 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py +++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py @@ -305,9 +305,11 @@ def create_token_response(self, request, token_handler): headers.update(e.headers) return headers, e.json, e.status_code - token = token_handler.create_token(request, refresh_token=self.refresh_token, save_token=False) + token = token_handler.create_token(request, refresh_token=self.refresh_token) + for modifier in self._token_modifiers: token = modifier(token, token_handler, request) + self.request_validator.save_token(token, request) self.request_validator.invalidate_authorization_code( request.client_id, request.code, request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py index c966795f..7e508577 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py @@ -76,10 +76,11 @@ def create_token_response(self, request, token_handler): headers.update(e.headers) return headers, e.json, e.status_code - token = token_handler.create_token(request, refresh_token=False, save_token=False) + token = token_handler.create_token(request, refresh_token=False) for modifier in self._token_modifiers: token = modifier(token) + self.request_validator.save_token(token, request) log.debug('Issuing token to client id %r (%r), %r.', diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py index d6de9061..48bae7a5 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py +++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py @@ -237,10 +237,13 @@ def create_token_response(self, request, token_handler): # "id_token token" - return the access token and the id token # "id_token" - don't return the access token if "token" in request.response_type.split(): - token = token_handler.create_token(request, refresh_token=False, save_token=False) + token = token_handler.create_token(request, refresh_token=False) else: token = {} + if request.state is not None: + token['state'] = request.state + for modifier in self._token_modifiers: token = modifier(token, token_handler, request) diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py index bd519e81..fc61d65c 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py +++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py @@ -64,10 +64,11 @@ def create_token_response(self, request, token_handler): return headers, e.json, e.status_code token = token_handler.create_token(request, - refresh_token=self.issue_new_refresh_tokens, save_token=False) + refresh_token=self.issue_new_refresh_tokens) for modifier in self._token_modifiers: token = modifier(token) + self.request_validator.save_token(token, request) log.debug('Issuing new token to client id %r (%r), %r.', diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py index f765d91b..5929afb4 100644 --- a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py +++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py @@ -104,10 +104,11 @@ def create_token_response(self, request, token_handler): headers.update(e.headers) return headers, e.json, e.status_code - token = token_handler.create_token(request, self.refresh_token, save_token=False) + token = token_handler.create_token(request, self.refresh_token) for modifier in self._token_modifiers: token = modifier(token) + self.request_validator.save_token(token, request) log.debug('Issuing token %r to client id %r (%r) and username %s.', diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index d78df09e..44a9a977 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -12,6 +12,7 @@ import hashlib import hmac from binascii import b2a_base64 +import warnings from oauthlib import common from oauthlib.common import add_params_to_qs, add_params_to_uri, unicode_type @@ -296,15 +297,18 @@ def __init__(self, request_validator=None, token_generator=None, ) self.expires_in = expires_in or 3600 - def create_token(self, request, refresh_token=False, save_token=True): + def create_token(self, request, refresh_token=False, **kwargs): """ Create a BearerToken, by default without refresh token. - + :param request: OAuthlib request. :type request: oauthlib.common.Request :param refresh_token: - :param save_token: """ + if "save_token" in kwargs: + warnings.warn("`save_token` has been deprecated, it was not used internally." + "If you do, use `request_validator.save_token()` instead.", + DeprecationWarning) if callable(self.expires_in): expires_in = self.expires_in(request) @@ -325,9 +329,6 @@ def create_token(self, request, refresh_token=False, save_token=True): if request.scopes is not None: token['scope'] = ' '.join(request.scopes) - if request.state is not None: - token['state'] = request.state - if refresh_token: if (request.refresh_token and not self.request_validator.rotate_refresh_token(request)): @@ -336,10 +337,7 @@ def create_token(self, request, refresh_token=False, save_token=True): token['refresh_token'] = self.refresh_token_generator(request) token.update(request.extra_credentials or {}) - token = OAuth2Token(token) - if save_token: - self.request_validator.save_bearer_token(token, request) - return token + return OAuth2Token(token) def validate_request(self, request): """ diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index fa578a57..05cdd376 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -58,9 +58,6 @@ def add_id_token(self, token, token_handler, request): if request.response_type and 'id_token' not in request.response_type: return token - if 'state' not in token: - token['state'] = request.state - if request.max_age: d = datetime.datetime.utcnow() token['auth_time'] = d.isoformat("T") + "Z" diff --git a/oauthlib/openid/connect/core/grant_types/implicit.py b/oauthlib/openid/connect/core/grant_types/implicit.py index 0eaa5b38..0a6fcb7f 100644 --- a/oauthlib/openid/connect/core/grant_types/implicit.py +++ b/oauthlib/openid/connect/core/grant_types/implicit.py @@ -26,3 +26,8 @@ def __init__(self, request_validator=None, **kwargs): self.custom_validators.post_auth.append( self.openid_implicit_authorization_validator) self.register_token_modifier(self.add_id_token) + + def add_id_token(self, token, token_handler, request): + if 'state' not in token: + token['state'] = request.state + return super(ImplicitGrant, self).add_id_token(token, token_handler, request) diff --git a/oauthlib/openid/connect/core/tokens.py b/oauthlib/openid/connect/core/tokens.py index 6b68891a..b67cdf2a 100644 --- a/oauthlib/openid/connect/core/tokens.py +++ b/oauthlib/openid/connect/core/tokens.py @@ -25,7 +25,7 @@ def __init__(self, request_validator=None, token_generator=None, ) self.expires_in = expires_in or 3600 - def create_token(self, request, refresh_token=False, save_token=False): + def create_token(self, request, refresh_token=False): """Create a JWT Token, using requestvalidator method.""" if callable(self.expires_in): diff --git a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py index 1a2f66bc..c77d18e9 100644 --- a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py +++ b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py @@ -42,18 +42,6 @@ def set_client(self, request): def test_state_preservation(self): auth_uri = 'http://example.com/path?state=xyz&client_id=abc&response_type=' - token_uri = 'http://example.com/path' - - # authorization grant - h, _, s = self.web.create_authorization_response( - auth_uri + 'code', scopes=['random']) - self.assertEqual(s, 302) - self.assertIn('Location', h) - code = get_query_credentials(h['Location'])['code'][0] - self.validator.validate_code.side_effect = self.set_state('xyz') - _, body, _ = self.web.create_token_response(token_uri, - body='grant_type=authorization_code&code=%s' % code) - self.assertEqual(json.loads(body)['state'], 'xyz') # implicit grant h, _, s = self.mobile.create_authorization_response( diff --git a/tests/oauth2/rfc6749/test_server.py b/tests/oauth2/rfc6749/test_server.py index b623a9ba..2c6ecff1 100644 --- a/tests/oauth2/rfc6749/test_server.py +++ b/tests/oauth2/rfc6749/test_server.py @@ -144,7 +144,7 @@ def set_user(request): @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): - body = 'grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' + body = 'grant_type=authorization_code&code=abc&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( '', body=body) token = { @@ -152,23 +152,27 @@ def test_authorization_grant(self): 'expires_in': self.expires_in, 'access_token': 'abc', 'refresh_token': 'abc', - 'scope': 'all of them', - 'state': 'xyz' + 'scope': 'all of them' } self.assertEqual(json.loads(body), token) - body = 'grant_type=authorization_code&code=abc&state=xyz' + body = 'grant_type=authorization_code&code=abc' headers, body, status_code = self.endpoint.create_token_response( '', body=body) token = { 'token_type': 'Bearer', 'expires_in': self.expires_in, 'access_token': 'abc', - 'refresh_token': 'abc', - 'state': 'xyz' + 'refresh_token': 'abc' } self.assertEqual(json.loads(body), token) + # try with additional custom variables + body = 'grant_type=authorization_code&code=abc&state=foobar' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + self.assertEqual(json.loads(body), token) + @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_password_grant(self): body = 'grant_type=password&username=a&password=hello&scope=all+of+them' @@ -277,7 +281,7 @@ def set_user(request): @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): - body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' + body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( '', body=body) body = json.loads(body) @@ -286,12 +290,11 @@ def test_authorization_grant(self): 'expires_in': self.expires_in, 'access_token': body['access_token'], 'refresh_token': 'abc', - 'scope': 'all of them', - 'state': 'xyz' + 'scope': 'all of them' } self.assertEqual(body, token) - body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&state=xyz' + body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc' headers, body, status_code = self.endpoint.create_token_response( '', body=body) body = json.loads(body) @@ -299,8 +302,20 @@ def test_authorization_grant(self): 'token_type': 'Bearer', 'expires_in': self.expires_in, 'access_token': body['access_token'], - 'refresh_token': 'abc', - 'state': 'xyz' + 'refresh_token': 'abc' + } + self.assertEqual(body, token) + + # try with additional custom variables + body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&state=foobar' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + body = json.loads(body) + token = { + 'token_type': 'Bearer', + 'expires_in': self.expires_in, + 'access_token': body['access_token'], + 'refresh_token': 'abc' } self.assertEqual(body, token) diff --git a/tests/openid/connect/core/test_server.py b/tests/openid/connect/core/test_server.py index ffab7b0b..756c9d0f 100644 --- a/tests/openid/connect/core/test_server.py +++ b/tests/openid/connect/core/test_server.py @@ -143,7 +143,7 @@ def set_user(request): @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_authorization_grant(self): - body = 'grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz' + body = 'grant_type=authorization_code&code=abc&scope=all+of+them' headers, body, status_code = self.endpoint.create_token_response( '', body=body) token = { @@ -151,23 +151,27 @@ def test_authorization_grant(self): 'expires_in': self.expires_in, 'access_token': 'abc', 'refresh_token': 'abc', - 'scope': 'all of them', - 'state': 'xyz' + 'scope': 'all of them' } self.assertEqual(json.loads(body), token) - body = 'grant_type=authorization_code&code=abc&state=xyz' + body = 'grant_type=authorization_code&code=abc' headers, body, status_code = self.endpoint.create_token_response( '', body=body) token = { 'token_type': 'Bearer', 'expires_in': self.expires_in, 'access_token': 'abc', - 'refresh_token': 'abc', - 'state': 'xyz' + 'refresh_token': 'abc' } self.assertEqual(json.loads(body), token) + # ignore useless fields + body = 'grant_type=authorization_code&code=abc&state=foobar' + headers, body, status_code = self.endpoint.create_token_response( + '', body=body) + self.assertEqual(json.loads(body), token) + def test_missing_type(self): _, body, _ = self.endpoint.create_token_response('', body='') token = {'error': 'unsupported_grant_type'} From 66d7c0035a8d33109ffaec9c8a620dd40255f99d Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 21 Feb 2019 10:01:29 +0100 Subject: [PATCH 221/237] Add clarity to the deprecation warning --- oauthlib/oauth2/rfc6749/tokens.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py index 44a9a977..79739238 100644 --- a/oauthlib/oauth2/rfc6749/tokens.py +++ b/oauthlib/oauth2/rfc6749/tokens.py @@ -306,8 +306,8 @@ def create_token(self, request, refresh_token=False, **kwargs): :param refresh_token: """ if "save_token" in kwargs: - warnings.warn("`save_token` has been deprecated, it was not used internally." - "If you do, use `request_validator.save_token()` instead.", + warnings.warn("`save_token` has been deprecated, it was not called internally." + "If you do, call `request_validator.save_token()` instead.", DeprecationWarning) if callable(self.expires_in): From c17a4a25a71b3b342ad522427c23038f417fb22e Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 21 Feb 2019 10:16:55 +0100 Subject: [PATCH 222/237] Add authorization "state" preservation back for AuthCode --- .../rfc6749/endpoints/test_credentials_preservation.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py index c77d18e9..c0cf86d8 100644 --- a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py +++ b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py @@ -43,6 +43,13 @@ def set_client(self, request): def test_state_preservation(self): auth_uri = 'http://example.com/path?state=xyz&client_id=abc&response_type=' + # authorization grant + h, _, s = self.web.create_authorization_response( + auth_uri + 'code', scopes=['random']) + self.assertEqual(s, 302) + self.assertIn('Location', h) + self.assertEqual(get_query_credentials(h['Location'])['state'][0], 'xyz') + # implicit grant h, _, s = self.mobile.create_authorization_response( auth_uri + 'token', scopes=['random']) From 87972cc4346a4c7b698fa1f026c76ddf717dec6b Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 21 Feb 2019 10:17:23 +0100 Subject: [PATCH 223/237] Removed useless set_state internal function Does not have purpose for /token request --- .../rfc6749/endpoints/test_credentials_preservation.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py index c0cf86d8..e7c66b67 100644 --- a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py +++ b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py @@ -29,12 +29,6 @@ def setUp(self): self.web = WebApplicationServer(self.validator) self.mobile = MobileApplicationServer(self.validator) - def set_state(self, state): - def set_request_state(client_id, code, client, request): - request.state = state - return True - return set_request_state - def set_client(self, request): request.client = mock.MagicMock() request.client.client_id = 'mocked' @@ -128,7 +122,7 @@ def test_default_uri_in_token(self): # was not given in the authorization AND not in the token request. self.validator.confirm_redirect_uri.return_value = True code = get_query_credentials(h['Location'])['code'][0] - self.validator.validate_code.side_effect = self.set_state('xyz') + self.validator.validate_code.return_value = True _, body, s = self.web.create_token_response(token_uri, body='grant_type=authorization_code&code=%s' % code) self.assertEqual(s, 200) From ff6844524b3fe28e7122a8177fb7c2e0993d5162 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 15:36:13 +0100 Subject: [PATCH 224/237] Change to 3.0.2-dev as long as master is in "dev" --- CHANGELOG.rst | 2 +- oauthlib/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9e0efdaf..dbd8c3fd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -TBD +3.0.2 (TBD) ------------------ * #652: Fixed OIDC /token response which wrongly returned "&state=None" diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index b23102c3..8eb82a65 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,6 +12,6 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.0.1' +__version__ = '3.0.2-dev' logging.getLogger('oauthlib').addHandler(NullHandler()) From 0d423ac7af419b69530cd05ab786527d941b4ffb Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 20:57:56 +0100 Subject: [PATCH 225/237] OIDC: Raise error=invalid_request when nonce is mandatory Until now, only OIDC implicit was raising an error, but OIDC hybrid contain a couple of mandatory nonce, too. --- .../openid/connect/core/grant_types/base.py | 23 ------- .../openid/connect/core/grant_types/hybrid.py | 25 ++++++++ .../connect/core/grant_types/implicit.py | 23 ++++++- .../connect/core/grant_types/test_implicit.py | 60 ++++++++++++++++--- 4 files changed, 99 insertions(+), 32 deletions(-) diff --git a/oauthlib/openid/connect/core/grant_types/base.py b/oauthlib/openid/connect/core/grant_types/base.py index 05cdd376..4f5c9445 100644 --- a/oauthlib/openid/connect/core/grant_types/base.py +++ b/oauthlib/openid/connect/core/grant_types/base.py @@ -247,28 +247,5 @@ def openid_authorization_validator(self, request): return request_info - def openid_implicit_authorization_validator(self, request): - """Additional validation when following the implicit flow. - """ - # Undefined in OpenID Connect, fall back to OAuth2 definition. - if request.response_type == 'token': - return {} - - # Treat it as normal OAuth 2 auth code request if openid is not present - if not request.scopes or 'openid' not in request.scopes: - return {} - - # REQUIRED. String value used to associate a Client session with an ID - # Token, and to mitigate replay attacks. The value is passed through - # unmodified from the Authentication Request to the ID Token. - # Sufficient entropy MUST be present in the nonce values used to - # prevent attackers from guessing values. For implementation notes, see - # Section 15.5.2. - if not request.nonce: - desc = 'Request is missing mandatory nonce parameter.' - raise InvalidRequestError(request=request, description=desc) - - return {} - OpenIDConnectBase = GrantTypeBase diff --git a/oauthlib/openid/connect/core/grant_types/hybrid.py b/oauthlib/openid/connect/core/grant_types/hybrid.py index 54669ae4..685fa08a 100644 --- a/oauthlib/openid/connect/core/grant_types/hybrid.py +++ b/oauthlib/openid/connect/core/grant_types/hybrid.py @@ -8,6 +8,7 @@ import logging from oauthlib.oauth2.rfc6749.grant_types.authorization_code import AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant +from oauthlib.oauth2.rfc6749.errors import InvalidRequestError from .base import GrantTypeBase from ..request_validator import RequestValidator @@ -34,3 +35,27 @@ def __init__(self, request_validator=None, **kwargs): self.register_code_modifier(self.add_token) self.register_code_modifier(self.add_id_token) self.register_token_modifier(self.add_id_token) + + def openid_authorization_validator(self, request): + """Additional validation when following the Authorization Code flow. + """ + request_info = super(HybridGrant, self).openid_authorization_validator(request) + if not request_info: # returns immediately if OAuth2.0 + return request_info + + # REQUIRED if the Response Type of the request is `code + # id_token` or `code id_token token` and OPTIONAL when the + # Response Type of the request is `code token`. It is a string + # value used to associate a Client session with an ID Token, + # and to mitigate replay attacks. The value is passed through + # unmodified from the Authentication Request to the ID + # Token. Sufficient entropy MUST be present in the `nonce` + # values used to prevent attackers from guessing values. For + # implementation notes, see Section 15.5.2. + if request.response_type in ["code id_token", "code id_token token"]: + if not request.nonce: + raise InvalidRequestError( + request=request, + description='Request is missing mandatory nonce parameter.' + ) + return request_info diff --git a/oauthlib/openid/connect/core/grant_types/implicit.py b/oauthlib/openid/connect/core/grant_types/implicit.py index 0a6fcb7f..d3797b28 100644 --- a/oauthlib/openid/connect/core/grant_types/implicit.py +++ b/oauthlib/openid/connect/core/grant_types/implicit.py @@ -10,6 +10,7 @@ from .base import GrantTypeBase from oauthlib.oauth2.rfc6749.grant_types.implicit import ImplicitGrant as OAuth2ImplicitGrant +from oauthlib.oauth2.rfc6749.errors import InvalidRequestError log = logging.getLogger(__name__) @@ -23,11 +24,29 @@ def __init__(self, request_validator=None, **kwargs): self.register_response_type('id_token token') self.custom_validators.post_auth.append( self.openid_authorization_validator) - self.custom_validators.post_auth.append( - self.openid_implicit_authorization_validator) self.register_token_modifier(self.add_id_token) def add_id_token(self, token, token_handler, request): if 'state' not in token: token['state'] = request.state return super(ImplicitGrant, self).add_id_token(token, token_handler, request) + + def openid_authorization_validator(self, request): + """Additional validation when following the implicit flow. + """ + request_info = super(ImplicitGrant, self).openid_authorization_validator(request) + if not request_info: # returns immediately if OAuth2.0 + return request_info + + # REQUIRED. String value used to associate a Client session with an ID + # Token, and to mitigate replay attacks. The value is passed through + # unmodified from the Authentication Request to the ID Token. + # Sufficient entropy MUST be present in the nonce values used to + # prevent attackers from guessing values. For implementation notes, see + # Section 15.5.2. + if not request.nonce: + raise InvalidRequestError( + request=request, + description='Request is missing mandatory nonce parameter.' + ) + return request_info diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py index 7ab198ac..54fd8b9d 100644 --- a/tests/openid/connect/core/grant_types/test_implicit.py +++ b/tests/openid/connect/core/grant_types/test_implicit.py @@ -4,6 +4,7 @@ import mock from oauthlib.common import Request +from oauthlib.oauth2.rfc6749 import errors from oauthlib.oauth2.rfc6749.tokens import BearerToken from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant @@ -30,8 +31,8 @@ def setUp(self): self.request.client_id = 'abcdef' self.request.response_type = 'id_token token' self.request.redirect_uri = 'https://a.b/cb' - self.request.nonce = 'zxc' self.request.state = 'abc' + self.request.nonce = 'xyz' self.mock_validator = mock.MagicMock() self.mock_validator.get_id_token.side_effect = get_id_token_mock @@ -61,12 +62,6 @@ def test_authorization(self, generate_token): self.assertEqual(b, None) self.assertEqual(s, 302) - self.request.nonce = None - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) - @mock.patch('oauthlib.common.generate_token') def test_no_prompt_authorization(self, generate_token): generate_token.return_value = 'abc' @@ -105,16 +100,41 @@ def test_no_prompt_authorization(self, generate_token): h, b, s = self.auth.create_authorization_response(self.request, bearer) self.assertIn('error=login_required', h['Location']) + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): def setUp(self): super(OpenIDHybridCodeTokenTest, self).setUp() self.request.response_type = 'code token' + self.request.nonce = None self.auth = HybridGrant(request_validator=self.mock_validator) self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' + @mock.patch('oauthlib.common.generate_token') + def test_optional_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = 'xyz' + scope, info = self.auth.validate_authorization_request(self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) + self.assertEqual(b, None) + self.assertEqual(s, 302) + class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): @@ -122,11 +142,24 @@ def setUp(self): super(OpenIDHybridCodeIdTokenTest, self).setUp() self.mock_validator.get_code_challenge.return_value = None self.request.response_type = 'code id_token' + self.request.nonce = 'zxc' self.auth = HybridGrant(request_validator=self.mock_validator) token = 'MOCKED_TOKEN' self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): @@ -134,7 +167,20 @@ def setUp(self): super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() self.mock_validator.get_code_challenge.return_value = None self.request.response_type = 'code id_token token' + self.request.nonce = 'xyz' self.auth = HybridGrant(request_validator=self.mock_validator) token = 'MOCKED_TOKEN' self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) From ad7b15428bde9eaa55bbc0ca0ce338342740a7c9 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:34:31 +0100 Subject: [PATCH 226/237] Add nonce auth request check for authorization_code --- .../core/grant_types/test_authorization_code.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/openid/connect/core/grant_types/test_authorization_code.py b/tests/openid/connect/core/grant_types/test_authorization_code.py index c3c78242..89401abe 100644 --- a/tests/openid/connect/core/grant_types/test_authorization_code.py +++ b/tests/openid/connect/core/grant_types/test_authorization_code.py @@ -40,6 +40,7 @@ def setUp(self): self.request.grant_type = 'authorization_code' self.request.redirect_uri = 'https://a.b/cb' self.request.state = 'abc' + self.request.nonce = None self.mock_validator = mock.MagicMock() self.mock_validator.authenticate_client.side_effect = self.set_client @@ -148,3 +149,16 @@ def test_create_token_response(self): self.assertIn('scope', token) self.assertNotIn('id_token', token) self.assertNotIn('openid', token['scope']) + + @mock.patch('oauthlib.common.generate_token') + def test_optional_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = 'xyz' + scope, info = self.auth.validate_authorization_request(self.request) + + bearer = BearerToken(self.mock_validator) + self.request.response_mode = 'query' + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_query) + self.assertEqual(b, None) + self.assertEqual(s, 302) From 2d9a89c23d0e0088ac84606e28be51f59f9fa12c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:34:48 +0100 Subject: [PATCH 227/237] Add nonce mandatory check for "id_token" response_type --- .../connect/core/grant_types/test_implicit.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py index 54fd8b9d..948edd34 100644 --- a/tests/openid/connect/core/grant_types/test_implicit.py +++ b/tests/openid/connect/core/grant_types/test_implicit.py @@ -113,6 +113,27 @@ def test_required_nonce(self, generate_token): self.assertEqual(s, 302) +class OpenIDImplicitNoAccessTokenTest(OpenIDImplicitTest): + def setUp(self): + super(OpenIDImplicitNoAccessTokenTest, self).setUp() + self.request.response_type = 'id_token' + token = 'MOCKED_TOKEN' + self.url_query = 'https://a.b/cb?state=abc&id_token=%s' % token + self.url_fragment = 'https://a.b/cb#state=abc&id_token=%s' % token + + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): def setUp(self): From 3ccaeb128156a2ab4519f177bf8811c89513862a Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:36:14 +0100 Subject: [PATCH 228/237] Move HybridGrant test into its respective file. --- .../connect/core/grant_types/test_hybrid.py | 75 ++++++++++++++++++ .../connect/core/grant_types/test_implicit.py | 76 +------------------ 2 files changed, 76 insertions(+), 75 deletions(-) diff --git a/tests/openid/connect/core/grant_types/test_hybrid.py b/tests/openid/connect/core/grant_types/test_hybrid.py index 6eb80370..8964053e 100644 --- a/tests/openid/connect/core/grant_types/test_hybrid.py +++ b/tests/openid/connect/core/grant_types/test_hybrid.py @@ -4,6 +4,8 @@ from tests.oauth2.rfc6749.grant_types.test_authorization_code import \ AuthorizationCodeGrantTest +from .test_authorization_code import OpenIDAuthCodeTest + class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): @@ -12,3 +14,76 @@ class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): def setUp(self): super(OpenIDHybridInterferenceTest, self).setUp() self.auth = HybridGrant(request_validator=self.mock_validator) + + +class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeTokenTest, self).setUp() + self.request.response_type = 'code token' + self.request.nonce = None + self.auth = HybridGrant(request_validator=self.mock_validator) + self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' + self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' + + @mock.patch('oauthlib.common.generate_token') + def test_optional_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = 'xyz' + scope, info = self.auth.validate_authorization_request(self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + +class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeIdTokenTest, self).setUp() + self.mock_validator.get_code_challenge.return_value = None + self.request.response_type = 'code id_token' + self.request.nonce = 'zxc' + self.auth = HybridGrant(request_validator=self.mock_validator) + token = 'MOCKED_TOKEN' + self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token + self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token + + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) + + +class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): + + def setUp(self): + super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() + self.mock_validator.get_code_challenge.return_value = None + self.request.response_type = 'code id_token token' + self.request.nonce = 'xyz' + self.auth = HybridGrant(request_validator=self.mock_validator) + token = 'MOCKED_TOKEN' + self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token + + @mock.patch('oauthlib.common.generate_token') + def test_required_nonce(self, generate_token): + generate_token.return_value = 'abc' + self.request.nonce = None + self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) + + bearer = BearerToken(self.mock_validator) + h, b, s = self.auth.create_authorization_response(self.request, bearer) + self.assertIn('error=invalid_request', h['Location']) + self.assertEqual(b, None) + self.assertEqual(s, 302) diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py index 948edd34..1ee805c1 100644 --- a/tests/openid/connect/core/grant_types/test_implicit.py +++ b/tests/openid/connect/core/grant_types/test_implicit.py @@ -7,11 +7,10 @@ from oauthlib.oauth2.rfc6749 import errors from oauthlib.oauth2.rfc6749.tokens import BearerToken from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt -from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant from tests.oauth2.rfc6749.grant_types.test_implicit import ImplicitGrantTest from tests.unittest import TestCase -from .test_authorization_code import get_id_token_mock, OpenIDAuthCodeTest +from .test_authorization_code import get_id_token_mock class OpenIDImplicitInterferenceTest(ImplicitGrantTest): @@ -132,76 +131,3 @@ def test_required_nonce(self, generate_token): self.assertIn('error=invalid_request', h['Location']) self.assertEqual(b, None) self.assertEqual(s, 302) - - -class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeTokenTest, self).setUp() - self.request.response_type = 'code token' - self.request.nonce = None - self.auth = HybridGrant(request_validator=self.mock_validator) - self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' - self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc' - - @mock.patch('oauthlib.common.generate_token') - def test_optional_nonce(self, generate_token): - generate_token.return_value = 'abc' - self.request.nonce = 'xyz' - scope, info = self.auth.validate_authorization_request(self.request) - - bearer = BearerToken(self.mock_validator) - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - -class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeIdTokenTest, self).setUp() - self.mock_validator.get_code_challenge.return_value = None - self.request.response_type = 'code id_token' - self.request.nonce = 'zxc' - self.auth = HybridGrant(request_validator=self.mock_validator) - token = 'MOCKED_TOKEN' - self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token - self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token - - @mock.patch('oauthlib.common.generate_token') - def test_required_nonce(self, generate_token): - generate_token.return_value = 'abc' - self.request.nonce = None - self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) - - bearer = BearerToken(self.mock_validator) - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) - - -class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest): - - def setUp(self): - super(OpenIDHybridCodeIdTokenTokenTest, self).setUp() - self.mock_validator.get_code_challenge.return_value = None - self.request.response_type = 'code id_token token' - self.request.nonce = 'xyz' - self.auth = HybridGrant(request_validator=self.mock_validator) - token = 'MOCKED_TOKEN' - self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token - self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token - - @mock.patch('oauthlib.common.generate_token') - def test_required_nonce(self, generate_token): - generate_token.return_value = 'abc' - self.request.nonce = None - self.assertRaises(errors.InvalidRequestError, self.auth.validate_authorization_request, self.request) - - bearer = BearerToken(self.mock_validator) - h, b, s = self.auth.create_authorization_response(self.request, bearer) - self.assertIn('error=invalid_request', h['Location']) - self.assertEqual(b, None) - self.assertEqual(s, 302) From 1ef4209f71d6abe20e71a65a4b9d1141205b087e Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:45:05 +0100 Subject: [PATCH 229/237] Added missing import after test moved --- tests/openid/connect/core/grant_types/test_hybrid.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/openid/connect/core/grant_types/test_hybrid.py b/tests/openid/connect/core/grant_types/test_hybrid.py index 8964053e..0aa0add6 100644 --- a/tests/openid/connect/core/grant_types/test_hybrid.py +++ b/tests/openid/connect/core/grant_types/test_hybrid.py @@ -1,13 +1,16 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals -from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant +import mock + +from oauthlib.oauth2.rfc6749 import errors +from oauthlib.oauth2.rfc6749.tokens import BearerToken +from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant from tests.oauth2.rfc6749.grant_types.test_authorization_code import \ AuthorizationCodeGrantTest from .test_authorization_code import OpenIDAuthCodeTest - class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest): """Test that OpenID don't interfere with normal OAuth 2 flows.""" From 4e945e9bcc847e77b7da37279853b853c4579cf7 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 25 Feb 2019 21:57:53 +0100 Subject: [PATCH 230/237] Notifications must be sent for every build I hope fixing the longstanding issue mentionned at https://github.com/oauthlib/oauthlib/issues/582. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c7978d7c..f2e68a6a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ notifications: urls: - https://coveralls.io/webhook - https://webhooks.gitter.im/e/6008c872bf0ecee344f4 - on_success: change + on_success: always on_failure: always on_start: never deploy: From 5b2bfd5d5de6525fbe12bbbf0dbe77a7640a6c09 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 4 Jul 2019 11:19:38 +0200 Subject: [PATCH 231/237] Update for 3.0.2 --- CHANGELOG.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dbd8c3fd..c5346ebd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,9 +1,12 @@ Changelog ========= -3.0.2 (TBD) +3.0.2 (2019-07-04) ------------------ +* #650: Fixed space encoding in base string URI used in the signature base string. * #652: Fixed OIDC /token response which wrongly returned "&state=None" +* #654: Doc: The value `state` must not be stored by the AS, only returned in /authorize response. +* #656: Fixed OIDC "nonce" checks: raise errors when it's mandatory 3.0.1 (2019-01-24) ------------------ From 9e824cfb0eb36b4d23ab73171b821b1a74ec659c Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Thu, 4 Jul 2019 11:22:12 +0200 Subject: [PATCH 232/237] Bump version --- oauthlib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 8eb82a65..089977c0 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,6 +12,6 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.0.2-dev' +__version__ = '3.0.2' logging.getLogger('oauthlib').addHandler(NullHandler()) From 0a9fd41faed16e15e04d6bfeef2b532d090f05bf Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 19 Jul 2019 12:42:43 +0200 Subject: [PATCH 233/237] Bump version --- oauthlib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index f1457a94..c7d19a16 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,7 +12,7 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.0.2' +__version__ = '3.1.0-dev' logging.getLogger('oauthlib').addHandler(NullHandler()) From 3002107b936352612c73c4334a7ba134029596f0 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 19 Jul 2019 15:35:46 +0200 Subject: [PATCH 234/237] Add 3.1.0 changelog --- CHANGELOG.rst | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c5346ebd..e07ec3ca 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,34 @@ Changelog ========= +3.1.0 (TBD) +------------------ + +OAuth2.0 Provider - Features +* #660: OIDC add support of nonce, c_hash, at_hash fields + - New RequestValidator.fill_id_token method + - Deprecated RequestValidator.get_id_token method +* #677: OIDC add UserInfo endpoint + - New RequestValidator.get_userinfo_claims method + +OAuth2.0 Provider - Security +* #665: Enhance data leak to logs + - New default to not expose request content in logs + - New function `oauthlib.set_debug(True)` +* #666: Disabling query parameters for POST requests + +OAuth2.0 Provider - Bugfixes +* #670: Fix validate_authorization_request to return the new PKCE fields +* #674: Fix token_type to be case-insensitive (bearer and Bearer) + +OAuth2.0 Client - Bugfixes +* #290: Fix Authorization Code's errors processing +* #603: BackendApplication.Client.prepare_request_body use the "scope" argument as intended. +* #672: Fix edge case when expires_in=Null + +OAuth1.0 Client +* #669: Add case-insensitive headers to oauth1 BaseEndpoint + 3.0.2 (2019-07-04) ------------------ * #650: Fixed space encoding in base string URI used in the signature base string. From a97cada2c456358242a4b099fcfed300d8173cf1 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 19 Jul 2019 15:41:30 +0200 Subject: [PATCH 235/237] Bump to 3.1.0 --- oauthlib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index c7d19a16..69dd1136 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,7 +12,7 @@ from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.1.0-dev' +__version__ = '3.1.0' logging.getLogger('oauthlib').addHandler(NullHandler()) From 36e4512762278abf4e21f0263a00eae8a22e1f1b Mon Sep 17 00:00:00 2001 From: Hamish Moffatt Date: Thu, 25 Jul 2019 13:49:05 +1000 Subject: [PATCH 236/237] add HMAC-SHA256 signature validation --- oauthlib/oauth1/rfc5849/endpoints/base.py | 7 ++++-- oauthlib/oauth1/rfc5849/signature.py | 30 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/oauthlib/oauth1/rfc5849/endpoints/base.py b/oauthlib/oauth1/rfc5849/endpoints/base.py index ecf8a50d..f005256b 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/base.py +++ b/oauthlib/oauth1/rfc5849/endpoints/base.py @@ -12,7 +12,7 @@ from oauthlib.common import CaseInsensitiveDict, Request, generate_token -from .. import (CONTENT_TYPE_FORM_URLENCODED, SIGNATURE_HMAC, SIGNATURE_RSA, +from .. import (CONTENT_TYPE_FORM_URLENCODED, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_RSA, SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY, errors, signature, utils) @@ -204,9 +204,12 @@ def _check_signature(self, request, is_token_request=False): resource_owner_secret = self.request_validator.get_access_token_secret( request.client_key, request.resource_owner_key, request) - if request.signature_method == SIGNATURE_HMAC: + if request.signature_method == SIGNATURE_HMAC_SHA1: valid_signature = signature.verify_hmac_sha1(request, client_secret, resource_owner_secret) + elif request.signature_method == SIGNATURE_HMAC_SHA256: + valid_signature = signature.verify_hmac_sha256(request, + client_secret, resource_owner_secret) else: valid_signature = signature.verify_plaintext(request, client_secret, resource_owner_secret) diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py index f899aca8..a60bee29 100644 --- a/oauthlib/oauth1/rfc5849/signature.py +++ b/oauthlib/oauth1/rfc5849/signature.py @@ -661,6 +661,36 @@ def verify_hmac_sha1(request, client_secret=None, return match +def verify_hmac_sha256(request, client_secret=None, + resource_owner_secret=None): + """Verify a HMAC-SHA256 signature. + + Per `section 3.4`_ of the spec. + + .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4 + + To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri + attribute MUST be an absolute URI whose netloc part identifies the + origin server or gateway on which the resource resides. Any Host + item of the request argument's headers dict attribute will be + ignored. + + .. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2 + + """ + norm_params = normalize_parameters(request.params) + bs_uri = base_string_uri(request.uri) + sig_base_str = signature_base_string(request.http_method, bs_uri, + norm_params) + signature = sign_hmac_sha256(sig_base_str, client_secret, + resource_owner_secret) + match = safe_string_equals(signature, request.signature) + if not match: + log.debug('Verify HMAC-SHA256 failed: signature base string: %s', + sig_base_str) + return match + + def _prepare_key_plus(alg, keystr): if isinstance(keystr, bytes): keystr = keystr.decode('utf-8') From eb8411004244a00c1100556f170eaaf134bc63ab Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Tue, 6 Aug 2019 00:43:18 +0200 Subject: [PATCH 237/237] Add the ReadTheDocs theme to dev builds "i.e tox -e docs". --- docs/conf.py | 2 +- tox.ini | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index f1a489ad..d18f2aa6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -98,7 +98,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/tox.ini b/tox.ini index 9cd2a9fb..60b0825c 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,9 @@ commands= [testenv:docs] basepython=python2.7 skipsdist=True -deps=sphinx +deps= + sphinx + sphinx_rtd_theme changedir=docs whitelist_externals=make commands=make clean html