diff --git a/provider/oauth2/backends.py b/provider/oauth2/backends.py index 5da5118..5c0e39f 100644 --- a/provider/oauth2/backends.py +++ b/provider/oauth2/backends.py @@ -1,6 +1,8 @@ +from django.contrib.auth import authenticate + from ..utils import now -from .forms import ClientAuthForm -from .models import AccessToken +from .forms import ClientAuthForm, PublicPasswordGrantForm +from .models import AccessToken, Client class BaseBackend(object): @@ -61,6 +63,44 @@ class RequestParamsClientBackend(object): return None +class RequestParamsClientBackend(object): + """ + Backend that tries to authenticate a client through request parameters + which might be in the request body or URI as defined in :rfc:`2.3.1`. + """ + def authenticate(self, request=None): + if request is None: + return None + + form = ClientAuthForm(request.REQUEST) + + if form.is_valid(): + return form.cleaned_data.get('client') + + return None + +class PublicPasswordBackend(object): + """ + Backend that tries to authenticate a client using username, password + and client ID. This is only available in specific circumstances: + + - grant_type is "password" + - client.client_type is 'public' + """ + + def authenticate(self, request=None): + if request is None: + return None + + form = PublicPasswordGrantForm(request.REQUEST) + + if form.is_valid(): + return form.cleaned_data.get('client') + + return None + + + class AccessTokenBackend(object): """ Authenticate a user via access token and client object. diff --git a/provider/oauth2/forms.py b/provider/oauth2/forms.py index 1e0485a..32ce019 100644 --- a/provider/oauth2/forms.py +++ b/provider/oauth2/forms.py @@ -301,3 +301,34 @@ class PasswordGrantForm(ScopeMixin, OAuthForm): data['user'] = user return data + +class PublicPasswordGrantForm(PasswordGrantForm): + client_id = forms.CharField(required=True) + grant_type = forms.CharField(required=True) + + def clean_grant_type(self): + grant_type = self.cleaned_data.get('grant_type') + + if grant_type != 'password': + raise OAuthValidationError({'error': 'invalid_grant'}) + + return grant_type + + def clean(self): + data = super(PublicPasswordGrantForm, self).clean() + + try: + client = Client.objects.get(client_id=data.get('client_id')) + except Client.DoesNotExist: + raise OAuthValidationError({'error': 'invalid_client'}) + + if client.client_type != 1: # public + raise OAuthValidationError({'error': 'invalid_client'}) + + data['client'] = client + return data + + + + + diff --git a/provider/oauth2/tests.py b/provider/oauth2/tests.py index e5ad8f5..34e0b92 100644 --- a/provider/oauth2/tests.py +++ b/provider/oauth2/tests.py @@ -334,21 +334,74 @@ class AccessTokenTest(BaseOAuth2TestCase): self.assertEqual('invalid_grant', json.loads(response.content)['error'], response.content) - def test_password_grant(self): + def test_password_grant_public(self): + c = self.get_client() + c.client_type = 1 # public + c.save() + response = self.client.post(self.access_token_url(), { 'grant_type': 'password', - 'client_id': self.get_client().client_id, - 'client_secret': self.get_client().client_secret, + 'client_id': c.client_id, + # No secret needed 'username': self.get_user().username, 'password': self.get_password(), }) self.assertEqual(200, response.status_code, response.content) + def test_password_grant_confidential(self): + c = self.get_client() + c.client_type = 0 # confidential + c.save() + response = self.client.post(self.access_token_url(), { 'grant_type': 'password', - 'client_id': self.get_client().client_id, - 'client_secret': self.get_client().client_secret, + 'client_id': c.client_id, + 'client_secret': c.client_secret, + 'username': self.get_user().username, + 'password': self.get_password(), + }) + + self.assertEqual(200, response.status_code, response.content) + + def test_password_grant_confidential_no_secret(self): + c = self.get_client() + c.client_type = 0 # confidential + c.save() + + response = self.client.post(self.access_token_url(), { + 'grant_type': 'password', + 'client_id': c.client_id, + 'username': self.get_user().username, + 'password': self.get_password(), + }) + + self.assertEqual('invalid_client', json.loads(response.content)['error']) + + def test_password_grant_invalid_password_public(self): + c = self.get_client() + c.client_type = 1 # public + c.save() + + response = self.client.post(self.access_token_url(), { + 'grant_type': 'password', + 'client_id': c.client_id, + 'username': self.get_user().username, + 'password': self.get_password() + 'invalid', + }) + + self.assertEqual(400, response.status_code, response.content) + self.assertEqual('invalid_client', json.loads(response.content)['error']) + + def test_password_grant_invalid_password_confidential(self): + c = self.get_client() + c.client_type = 0 # confidential + c.save() + + response = self.client.post(self.access_token_url(), { + 'grant_type': 'password', + 'client_id': c.client_id, + 'client_secret': c.client_secret, 'username': self.get_user().username, 'password': self.get_password() + 'invalid', }) diff --git a/provider/oauth2/views.py b/provider/oauth2/views.py index f90871e..0c3af84 100644 --- a/provider/oauth2/views.py +++ b/provider/oauth2/views.py @@ -7,7 +7,7 @@ from .forms import AuthorizationRequestForm, AuthorizationForm from .forms import PasswordGrantForm, RefreshTokenGrantForm from .forms import AuthorizationCodeGrantForm from .models import Client, RefreshToken, AccessToken -from .backends import BasicClientBackend, RequestParamsClientBackend +from .backends import BasicClientBackend, RequestParamsClientBackend, PublicPasswordBackend class Capture(Capture): @@ -70,6 +70,7 @@ class AccessTokenView(AccessTokenView): authentication = ( BasicClientBackend, RequestParamsClientBackend, + PublicPasswordBackend, ) def get_authorization_code_grant(self, request, data, client):