From 39111a83884ed5b5cad4878e787e156918bb89f9 Mon Sep 17 00:00:00 2001 From: Niels van Huijstee Date: Tue, 13 Dec 2016 13:40:14 +0100 Subject: [PATCH] Better tests for errors, disable grant type password by default --- oidc_provider/lib/endpoints/token.py | 6 ++++- oidc_provider/lib/errors.py | 15 +++++++++++++ oidc_provider/settings.py | 16 +++++++++++++ oidc_provider/tests/test_token_endpoint.py | 26 +++++++++++++++++++++- oidc_provider/views.py | 4 ++-- 5 files changed, 63 insertions(+), 4 deletions(-) diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index 327ed81..5503b4c 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -14,6 +14,7 @@ from django.http import JsonResponse from oidc_provider.lib.errors import ( TokenError, + UserAuthError, ) from oidc_provider.lib.utils.token import ( create_id_token, @@ -123,13 +124,16 @@ class TokenEndpoint(object): raise TokenError('invalid_grant') elif self.params['grant_type'] == 'password': + if not settings.get('OIDC_GRANT_TYPE_PASSWORD_ENABLE'): + raise TokenError('unsupported_grant_type') + user = authenticate( username=self.params['username'], password=self.params['password'] ) if not user: - raise TokenError('Invalid user credentials') + raise UserAuthError() self.user = user diff --git a/oidc_provider/lib/errors.py b/oidc_provider/lib/errors.py index ce84811..47f4b10 100644 --- a/oidc_provider/lib/errors.py +++ b/oidc_provider/lib/errors.py @@ -16,6 +16,21 @@ class ClientIdError(Exception): description = 'The client identifier (client_id) is missing or invalid.' +class UserAuthError(Exception): + """ + Specific to the Resource Owner Password Credentials flow when + the Resource Owners credentials are not valid. + """ + error = 'access_denied' + description = 'The resource owner or authorization server denied ' \ + 'the request' + + def create_dict(self): + return { + 'error': self.error, + 'error_description': self.description, + } + class AuthorizeError(Exception): _errors = { diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 610534c..c277d3c 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -115,6 +115,22 @@ class DefaultSettings(object): """ return 'oidc_provider.lib.utils.common.default_idtoken_processing_hook' + @property + def OIDC_GRANT_TYPE_PASSWORD_ENABLE(self): + """ + OPTIONAL. A boolean to set whether to allow the Resource Owner Password + Credentials Grant. https://tools.ietf.org/html/rfc6749#section-4.3 + + From the specification: + Since this access token request utilizes the resource owner's + password, the authorization server MUST protect the endpoint + against brute force attacks (e.g., using rate-limitation or + generating alerts). + + How you do this, is up to you. + """ + return False + default_settings = DefaultSettings() diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 1da2c07..8a3ca22 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -164,6 +164,20 @@ class TokenTestCase(TestCase): # o validate the resource owner password credentials using its # existing password validation algorithm. + def test_default_setting_does_not_allow_grant_type_password(self): + post_data = self._password_grant_post_data() + + response = self._post_request( + post_data=post_data, + extras=self._auth_header() + ) + + response_dict = json.loads(response.content.decode('utf-8')) + + self.assertEqual(400, response.status_code) + self.assertEqual('unsupported_grant_type', response_dict['error']) + + @override_settings(OIDC_GRANT_TYPE_PASSWORD_ENABLE=True) def test_password_grant_get_access_token_without_scope(self): post_data = self._password_grant_post_data() del (post_data['scope']) @@ -176,6 +190,7 @@ class TokenTestCase(TestCase): response_dict = json.loads(response.content.decode('utf-8')) self.assertIn('access_token', response_dict) + @override_settings(OIDC_GRANT_TYPE_PASSWORD_ENABLE=True) def test_password_grant_get_access_token_with_scope(self): response = self._post_request( post_data=self._password_grant_post_data(), @@ -185,6 +200,7 @@ class TokenTestCase(TestCase): response_dict = json.loads(response.content.decode('utf-8')) self.assertIn('access_token', response_dict) + @override_settings(OIDC_GRANT_TYPE_PASSWORD_ENABLE=True) def test_password_grant_get_access_token_invalid_user_credentials(self): invalid_post = self._password_grant_post_data() invalid_post['password'] = 'wrong!' @@ -194,7 +210,11 @@ class TokenTestCase(TestCase): extras=self._auth_header() ) + response_dict = json.loads(response.content.decode('utf-8')) + print(response_dict) + self.assertEqual(400, response.status_code) + self.assertEqual('access_denied', response_dict['error']) def test_password_grant_get_access_token_invalid_client_credentials(self): self.client.client_id = 'foo' @@ -205,10 +225,14 @@ class TokenTestCase(TestCase): extras=self._auth_header() ) + response_dict = json.loads(response.content.decode('utf-8')) + self.assertEqual(400, response.status_code) + self.assertEqual('invalid_client', response_dict['error']) @patch('oidc_provider.lib.utils.token.uuid') - @override_settings(OIDC_TOKEN_EXPIRE=120) + @override_settings(OIDC_TOKEN_EXPIRE=120, + OIDC_GRANT_TYPE_PASSWORD_ENABLE=True) def test_password_grant_full_response(self, mock_uuid): test_hex = 'fake_token' mock_uuid4 = Mock(spec=uuid.uuid4) diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 6bf142f..1c94d05 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -28,7 +28,7 @@ from oidc_provider.lib.errors import ( ClientIdError, RedirectUriError, TokenError, -) + UserAuthError) from oidc_provider.lib.utils.common import ( redirect, get_site_url, @@ -167,7 +167,7 @@ class TokenView(View): return TokenEndpoint.response(dic) - except (TokenError) as error: + except (TokenError, UserAuthError) as error: return TokenEndpoint.response(error.create_dict(), status=400)