Removing need to client_secret during password grants for public clients

As discussed in issues #21 and #25. This provides a more rigerous check
of the circumstances required for the client_secret to forgone.
This commit is contained in:
Adam Charnock 2013-09-14 12:54:19 +01:00 committed by Evan Culver
parent cdce08d257
commit ad7d4f22e1
4 changed files with 133 additions and 8 deletions

View File

@ -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.

View File

@ -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

View File

@ -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',
})

View File

@ -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):