diff --git a/doac/exceptions/invalid_request.py b/doac/exceptions/invalid_request.py index 882a247..2a50293 100644 --- a/doac/exceptions/invalid_request.py +++ b/doac/exceptions/invalid_request.py @@ -13,6 +13,14 @@ class AuthorizationCodeNotValid(InvalidRequest): reason = "The authorization code was malformed or invalid." +class ClientCredentialsNotProvided(InvalidRequest): + reason = "The client credentials were not provided." + + +class ClientCredentialsNotValid(InvalidRequest): + reason = "The client credentials were malformed or invalid." + + class ClientNotProvided(InvalidRequest): reason = "The client was not provided." diff --git a/doac/views.py b/doac/views.py index 9f106bc..3cb1a44 100644 --- a/doac/views.py +++ b/doac/views.py @@ -8,25 +8,26 @@ ALLOWED_RESPONSE_TYPES = ("code", "token", ) -ALLOWED_GRANT_TYPES = ("authorization_code", "refresh_token", ) +ALLOWED_GRANT_TYPES = ("authorization_code", "refresh_token", "password", ) class OAuthView(View): """ All views must subclass this class. - This provides common methods which are needed for validation and processing OAuth - requests and responses. + This provides common methods which are needed for validation and + processing OAuth requests and responses. """ def handle_exception(self, exception): """ - Handle a unspecified exception and return the correct method that should be used - for handling it. + Handle a unspecified exception and return the correct method that + should be used for handling it. If the exception has the `can_redirect` property set to False, it is - rendered to the browser. Otherwise, it will be redirected to the location - provided in the `RedirectUri` object that is associated with the request. + rendered to the browser. Otherwise, it will be redirected to the + location provided in the `RedirectUri` object that is associated with + the request. """ can_redirect = getattr(exception, "can_redirect", True) @@ -63,7 +64,8 @@ def render_exception(self, exception): def render_exception_js(self, exception): """ - Return a response with the body containing a JSON-formatter version of the exception. + Return a response with the body containing a JSON-formatter version of + the exception. """ from .http import JsonResponse @@ -76,12 +78,13 @@ def render_exception_js(self, exception): def verify_dictionary(self, dict, *args): """ - Based on a provided `dict`, validate all of the contents of that dictionary that are - provided. + Based on a provided `dict`, validate all of the contents of that + dictionary that are provided. - For each argument provided that isn't the dictionary, this will set the raw value of - that key as the instance variable of the same name. It will then call the verification - function named `verify_[argument]` to verify the data. + For each argument provided that isn't the dictionary, this will set the + raw value of that key as the instance variable of the same name. It + will then call the verification function named `verify_[argument]` to + verify the data. """ for arg in args: @@ -93,8 +96,8 @@ def verify_dictionary(self, dict, *args): def verify_client_id(self): """ - Verify a provided client id against the database and set the `Client` object that is - associated with it to `self.client`. + Verify a provided client id against the database and set the `Client` + object that is associated with it to `self.client`. TODO: Document all of the thrown exceptions. """ @@ -106,7 +109,8 @@ def verify_client_id(self): if self.client_id: try: self.client = Client.objects.for_id(self.client_id) - # Catching also ValueError for the case when client_id doesn't contain an integer. + # Catching also ValueError for the case when client_id doesn't + # contain an integer. except (Client.DoesNotExist, ValueError): raise ClientDoesNotExist() else: @@ -115,7 +119,8 @@ def verify_client_id(self): def verify_redirect_uri(self): from urlparse import urlparse from .models import RedirectUri - from .exceptions.invalid_request import RedirectUriDoesNotValidate, RedirectUriNotProvided + from .exceptions.invalid_request import RedirectUriDoesNotValidate, \ + RedirectUriNotProvided PARSE_MATCH_ATTRIBUTES = ("scheme", "hostname", "port", ) @@ -133,7 +138,8 @@ def verify_redirect_uri(self): raise RedirectUriDoesNotValidate() try: - self.redirect_uri = RedirectUri.objects.with_client(self.client).for_url(self.redirect_uri) + self.redirect_uri = RedirectUri.objects \ + .with_client(self.client).for_url(self.redirect_uri) except RedirectUri.DoesNotExist: raise RedirectUriDoesNotValidate() else: @@ -166,7 +172,8 @@ def authorization_accepted(self): from django.http import HttpResponseRedirect from .models import AuthorizationToken - self.authorization_token = AuthorizationToken(user=self.request.user, client=self.client) + self.authorization_token = AuthorizationToken(user=self.request.user, + client=self.client) self.authorization_token.save() self.authorization_token.scope = self.scopes @@ -177,11 +184,14 @@ def authorization_accepted(self): else: separator = "#" - self.access_token = self.authorization_token.generate_refresh_token().generate_access_token() + self.access_token = self.authorization_token \ + .generate_refresh_token() \ + .generate_access_token() query_string = self.generate_query_string() - return HttpResponseRedirect(self.redirect_uri.url + separator + query_string) + return HttpResponseRedirect(self.redirect_uri.url + separator + + query_string) def authorization_denied(self): from .exceptions.access_denied import AuthorizationDenied @@ -203,7 +213,8 @@ def generate_query_string(self): def verify_code(self): from .models import AuthorizationCode - from .exceptions.invalid_request import AuthorizationCodeNotValid, AuthorizationCodeNotProvided + from .exceptions.invalid_request import AuthorizationCodeNotValid, \ + AuthorizationCodeNotProvided if self.code: get_code = self.request.GET.get("code", None) @@ -212,7 +223,8 @@ def verify_code(self): raise AuthorizationCodeNotValid() try: - self.authorization_code = AuthorizationCode.objects.for_token(self.code) + self.authorization_code = AuthorizationCode.objects \ + .for_token(self.code) except AuthorizationCode.DoesNotExist: raise AuthorizationCodeNotValid() else: @@ -231,7 +243,8 @@ def get(self, request, *args, **kwargs): self.state = request.GET.get("state", "o2cs") try: - self.verify_dictionary(request.GET, "client_id", "redirect_uri", "scope", "response_type") + self.verify_dictionary(request.GET, "client_id", "redirect_uri", + "scope", "response_type") except Exception, e: return self.handle_exception(e) @@ -253,7 +266,9 @@ def get(self, request, *args, **kwargs): def generate_authorization_code(self): from .models import AuthorizationCode - code = AuthorizationCode(client=self.client, redirect_uri=self.redirect_uri, response_type=self.response_type) + code = AuthorizationCode(client=self.client, + redirect_uri=self.redirect_uri, + response_type=self.response_type) code.save() code.scope = self.scopes @@ -300,7 +315,8 @@ def dispatch(self, *args, **kwargs): def post(self, request, *args, **kwargs): try: - self.verify_dictionary(request.POST, "grant_type", "client_id", "client_secret") + self.verify_dictionary(request.POST, "grant_type", "client_id", + "client_secret") except Exception, e: return self.render_exception_js(e) @@ -310,7 +326,8 @@ def post(self, request, *args, **kwargs): except Exception, e: return self.render_exception_js(e) - self.refresh_token = self.authorization_token.generate_refresh_token() + self.refresh_token = self.authorization_token \ + .generate_refresh_token() if not self.refresh_token: self.authorization_token.revoke_tokens() @@ -329,6 +346,15 @@ def post(self, request, *args, **kwargs): return self.render_refresh_token() + elif self.grant_type == "password": + try: + self.verify_dictionary(request.POST, "scope") + self.verify_user() + except Exception, e: + return self.render_exception_js(e) + + return self.render_password() + def render_authorization_token(self): from .compat import now from .http import JsonResponse @@ -356,6 +382,29 @@ def render_refresh_token(self): return JsonResponse(response) + def render_password(self): + from doac.compat import now + from doac.http import JsonResponse + from doac.models import AuthorizationToken + + self.authorization_token = AuthorizationToken(user=self.user, + client=self.client) + self.authorization_token.save() + + self.refresh_token = self.authorization_token.generate_refresh_token() + self.access_token = self.refresh_token.generate_access_token() + + remaining = self.access_token.expires_at - now() + + response = { + "access_token": self.access_token.token, + "token_type": "bearer", + "expires_in": int(total_seconds(remaining)), + "refresh_token": self.refresh_token.token, + } + + return JsonResponse(response) + def verify_client_secret(self): from .exceptions.invalid_client import ClientSecretNotValid from .exceptions.invalid_request import ClientSecretNotProvided @@ -367,12 +416,14 @@ def verify_client_secret(self): raise ClientSecretNotProvided() def verify_code(self): - from .exceptions.invalid_request import AuthorizationCodeAlreadyUsed, AuthorizationCodeNotProvided, AuthorizationCodeNotValid + from .exceptions.invalid_request import AuthorizationCodeAlreadyUsed, \ + AuthorizationCodeNotProvided, AuthorizationCodeNotValid from .models import AuthorizationToken if self.code: try: - self.authorization_token = AuthorizationToken.objects.with_client(self.client).for_token(self.code) + self.authorization_token = AuthorizationToken.objects \ + .with_client(self.client).for_token(self.code) if not self.authorization_token.is_active: self.authorization_token.revoke_tokens() @@ -384,7 +435,8 @@ def verify_code(self): raise AuthorizationCodeNotProvided() def verify_grant_type(self): - from .exceptions.unsupported_grant_type import GrantTypeNotProvided, GrantTypeNotValid + from .exceptions.unsupported_grant_type import GrantTypeNotProvided, \ + GrantTypeNotValid self.grant_type = self.request.POST.get("grant_type", None) @@ -395,13 +447,31 @@ def verify_grant_type(self): raise GrantTypeNotProvided() def verify_refresh_token(self): - from .exceptions.invalid_request import RefreshTokenNotProvided, RefreshTokenNotValid + from .exceptions.invalid_request import RefreshTokenNotProvided, \ + RefreshTokenNotValid from .models import RefreshToken if self.refresh_token: try: - self.refresh_token = RefreshToken.objects.with_client(self.client).for_token(self.refresh_token) + self.refresh_token = RefreshToken.objects \ + .with_client(self.client).for_token(self.refresh_token) except RefreshToken.DoesNotExist: raise RefreshTokenNotValid() else: raise RefreshTokenNotProvided() + + def verify_user(self): + from django.contrib.auth import authenticate + from doac.exceptions.invalid_request import \ + ClientCredentialsNotProvided, ClientCredentialsNotValid + + username = self.request.POST.get("username", None) + password = self.request.POST.get("password", None) + + if not username or not password: + raise ClientCredentialsNotProvided() + + self.user = authenticate(username=username, password=password) + + if not self.user or not self.user.is_active: + raise ClientCredentialsNotValid() diff --git a/tests/tests/views/test_token.py b/tests/tests/views/test_token.py index 7cff792..54379d6 100644 --- a/tests/tests/views/test_token.py +++ b/tests/tests/views/test_token.py @@ -7,76 +7,149 @@ class TestTokenErrors(TokenTestCase): - + def test_grant_type(self): - from doac.exceptions.unsupported_grant_type import GrantTypeNotProvided, GrantTypeNotValid - - request = self.client.post(reverse("oauth2_token")) + from doac.exceptions.unsupported_grant_type import \ + GrantTypeNotProvided, GrantTypeNotValid + + data = {} + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, GrantTypeNotProvided()) - - request = self.client.post(reverse("oauth2_token"), {"grant_type": ""}) + + data["grant_type"] = "" + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, GrantTypeNotProvided()) - - request = self.client.post(reverse("oauth2_token"), {"grant_type": "invalid"}) + + data["grant_type"] = "invalid" + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, GrantTypeNotValid()) - + def test_client_id(self): from doac.exceptions.invalid_request import ClientNotProvided from doac.exceptions.invalid_client import ClientDoesNotExist - - request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code"}) + + data = { + "grant_type": "authorization_code", + } + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, ClientNotProvided()) - - request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": ""}) + + data["client_id"] = "" + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, ClientNotProvided()) - - request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": 1234}) + + data["client_id"] = "1234" + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, ClientDoesNotExist()) - + def test_client_secret(self): from doac.exceptions.invalid_client import ClientSecretNotValid from doac.exceptions.invalid_request import ClientSecretNotProvided - - request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id}) + + data = { + "grant_type": "authorization_code", + "client_id": self.oauth_client.id, + } + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, ClientSecretNotProvided()) - - request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id, "client_secret": ""}) + + data["client_secret"] = "" + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, ClientSecretNotProvided()) - - request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id, "client_secret": "notVerySecret"}) + + data["client_secret"] = "notVerySecret" + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, ClientSecretNotValid()) - + def test_code(self): - from doac.exceptions.invalid_request import AuthorizationCodeAlreadyUsed, AuthorizationCodeNotProvided, AuthorizationCodeNotValid - - request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id, "client_secret": self.client_secret}) + from doac.exceptions.invalid_request import \ + AuthorizationCodeAlreadyUsed, AuthorizationCodeNotProvided, \ + AuthorizationCodeNotValid + + data = { + "grant_type": "authorization_code", + "client_id": self.oauth_client.id, + "client_secret": self.client_secret, + } + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, AuthorizationCodeNotProvided()) - - request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id, "client_secret": self.client_secret, "code": ""}) + + data["code"] = "" + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, AuthorizationCodeNotProvided()) - - request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id, "client_secret": self.client_secret, "code": "invalid"}) + + data["code"] = "invalid" + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, AuthorizationCodeNotValid()) - - request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id, "client_secret": self.client_secret, "code": self.authorization_token.token}) - request = self.client.post(reverse("oauth2_token"), {"grant_type": "authorization_code", "client_id": self.oauth_client.id, "client_secret": self.client_secret, "code": self.authorization_token.token}) + + data["code"] = self.authorization_token.token + + request = self.client.post(reverse("oauth2_token"), data) + request = self.client.post(reverse("oauth2_token"), data) + self.assertExceptionJson(request, AuthorizationCodeAlreadyUsed()) - + def test_refresh_token(self): - from doac.exceptions.invalid_request import RefreshTokenNotProvided, RefreshTokenNotValid - - request = self.client.post(reverse("oauth2_token"), {"grant_type": "refresh_token", "client_id": self.oauth_client.id, "client_secret": self.client_secret}) + from doac.exceptions.invalid_request import \ + RefreshTokenNotProvided, RefreshTokenNotValid + + data = { + "grant_type": "refresh_token", + "client_id": self.oauth_client.id, + "client_secret": self.client_secret, + } + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, RefreshTokenNotProvided()) - - request = self.client.post(reverse("oauth2_token"), {"grant_type": "refresh_token", "client_id": self.oauth_client.id, "client_secret": self.client_secret, "refresh_token": ""}) + + data["refresh_token"] = "" + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, RefreshTokenNotProvided()) - - request = self.client.post(reverse("oauth2_token"), {"grant_type": "refresh_token", "client_id": self.oauth_client.id, "client_secret": self.client_secret, "refresh_token": "invalid"}) + + data["refresh_token"] = "invalid" + + request = self.client.post(reverse("oauth2_token"), data) self.assertExceptionJson(request, RefreshTokenNotValid()) + def test_password_authentication(self): + from doac.exceptions.invalid_request import \ + ClientCredentialsNotProvided, ClientCredentialsNotValid + + data = { + "grant_type": "password", + "client_id": self.oauth_client.id, + "client_secret": self.client_secret, + } + + request = self.client.post(reverse("oauth2_token"), data) + self.assertExceptionJson(request, ClientCredentialsNotProvided()) + + data["username"] = self.user.username + + request = self.client.post(reverse("oauth2_token"), data) + self.assertExceptionJson(request, ClientCredentialsNotProvided()) + + data["password"] = "not valid" + + request = self.client.post(reverse("oauth2_token"), data) + self.assertExceptionJson(request, ClientCredentialsNotValid()) + class TestTokenResponse(TokenTestCase): - + def test_authorization_token(self): data = { "grant_type": "authorization_code", @@ -84,36 +157,68 @@ def test_authorization_token(self): "client_secret": self.client_secret, "code": self.authorization_token.token, } - + request = self.client.post(reverse("oauth2_token"), data) - + + refresh_token = self.authorization_token.refresh_token + access_token = refresh_token.access_tokens.all()[0] + response = { - "refresh_token": self.authorization_token.refresh_token.token, + "refresh_token": refresh_token.token, "token_type": "bearer", "expires_in": 5183999, - "access_token": self.authorization_token.refresh_token.access_tokens.all()[0].token, + "access_token": access_token.token, } - + self.assertEqual(request.content, json.dumps(response)) self.assertEqual(request.status_code, 200) - + def test_refresh_token(self): refresh_token = self.authorization_token.generate_refresh_token() - + data = { "grant_type": "refresh_token", "client_id": self.oauth_client.id, "client_secret": self.client_secret, "refresh_token": refresh_token.token, } - + request = self.client.post(reverse("oauth2_token"), data) - + response = { "token_type": "bearer", "expires_in": 7199, "access_token": refresh_token.access_tokens.all()[0].token, } - + self.assertEqual(request.content, json.dumps(response)) self.assertEqual(request.status_code, 200) + + def test_password(self): + from doac.models import RefreshToken + + data = { + "grant_type": "password", + "client_id": self.oauth_client.id, + "client_secret": self.client_secret, + "username": self.user.username, + "password": "test", + } + + request = self.client.post(reverse("oauth2_token"), data) + + self.assertEqual(RefreshToken.objects.count(), 1) + + refresh_token = RefreshToken.objects.get() + access_token = refresh_token.access_tokens.get() + + self.assertEqual(request.status_code, 200) + + response = { + "access_token": access_token.token, + "token_type": "bearer", + "expires_in": 7199, + "refresh_token": refresh_token.token, + } + + self.assertEqual(request.content, json.dumps(response))