From 24ec32e941daf5b034f658870aa682e8b2776378 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Tue, 21 Feb 2017 16:32:34 +0100 Subject: [PATCH 1/9] Added new web token authorization for FooCards --- src/authtoken/authentication.py | 80 +++++++++-- src/authtoken/serializers.py | 17 +++ src/authtoken/test.py | 226 ++++++++++++++++++++++++++++++++ src/authtoken/views.py | 36 +++++ src/foobar/settings/base.py | 1 + 5 files changed, 351 insertions(+), 9 deletions(-) create mode 100644 src/authtoken/serializers.py create mode 100644 src/authtoken/test.py create mode 100644 src/authtoken/views.py diff --git a/src/authtoken/authentication.py b/src/authtoken/authentication.py index f7da739..41caab3 100644 --- a/src/authtoken/authentication.py +++ b/src/authtoken/authentication.py @@ -1,7 +1,33 @@ +import jwt +from jwt.exceptions import InvalidKeyError, InvalidTokenError +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ from rest_framework import authentication, exceptions from rest_framework.authentication import get_authorization_header -from django.utils.translation import ugettext_lazy as _ + from .models import Token +from foobar.models import Account + + +def validate_header(auth): + if len(auth) == 1: + msg = _('Invalid token header. No credentials provided.') + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: + msg = _('Invalid token header. Token string should not contain ' + 'spaces.') + raise exceptions.AuthenticationFailed(msg) + + +class FallbackAuthentication(authentication.BaseAuthentication): + """ + This is a fallback intended to run as a last resort for authentication. + It does however only raise an exception, telling the requestor that all + attempts on authenticating has failed. + """ + + def authenticate(self): + raise exceptions.AuthenticationFailed(_('Invalid token')) class TokenAuthentication(authentication.BaseAuthentication): @@ -24,14 +50,7 @@ def authenticate(self, request): if not auth or auth[0].lower() != b'token': return None - if len(auth) == 1: - msg = _('Invalid token header. No credentials provided.') - raise exceptions.AuthenticationFailed(msg) - elif len(auth) > 2: - msg = _('Invalid token header. Token string should not contain ' - 'spaces.') - raise exceptions.AuthenticationFailed(msg) - + validate_header(auth) try: token = auth[1].decode() except UnicodeError: @@ -51,3 +70,46 @@ def authenticate_credentials(self, key): def authenticate_header(self, request): return 'Token' + + +class FooCardAuthentication(authentication.BaseAuthentication): + """ + JSON Web Token authentication. + Acts identically to token based authentication, as seen from the outside. + Clients should authenticate by passing the token key in the "Authorization" + HTTP header, prepended with the string "CardToken ". For example: + Authorization: CardToken: 401f7ac837da42b97f613d789819ff93537bee6a + """ + algorithm = 'HS256' + message = 'Invalid token' + model = Account + + def authenticate(self, request): + auth = get_authorization_header(request).split() + if not auth or auth[0].lower() != b'cardtoken': + return None + + validate_header(auth) + try: + payload = jwt.decode( + auth[1].decode(), + settings.SECRET_WEBTOKEN, + algorithm=self.algorithm + ) + except (InvalidKeyError, InvalidTokenError): + msg = _('{0} header'.format(self.message)) + raise exceptions.AuthenticationFailed(msg) + + return self.authenticate_credentials(payload) + + def authenticate_credentials(self, payload): + account_id = payload.get('account_id') + try: + account = self.model.objects.get(pk=account_id) + except self.model.DoesNotExist: + raise exceptions.AuthenticationFailed(_(self.message)) + + return (account, None) + + def authenticate_header(self, request): + return 'CardToken' diff --git a/src/authtoken/serializers.py b/src/authtoken/serializers.py new file mode 100644 index 0000000..12131e7 --- /dev/null +++ b/src/authtoken/serializers.py @@ -0,0 +1,17 @@ +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from foobar.api import get_account + + +class CardTokenSerializer(serializers.Serializer): + number = serializers.CharField(required=True, allow_blank=False) + + def validate(self, attrs): + account = get_account(attrs.get('number')) + if account is None: + msg = {'number': _('Card is not registered')} + raise serializers.ValidationError(msg) + + attrs['account'] = account + return attrs diff --git a/src/authtoken/test.py b/src/authtoken/test.py new file mode 100644 index 0000000..9f014d0 --- /dev/null +++ b/src/authtoken/test.py @@ -0,0 +1,226 @@ +import datetime +import jwt + +from django.conf.urls import url +from django.http import HttpResponse +from django.test import override_settings, TestCase +from django.utils import timezone +from rest_framework import status +from rest_framework.test import APIClient, APIRequestFactory +from rest_framework.views import APIView + +from .authentication import FooCardAuthentication +from .models import Token +from .views import obtain_foocard_token +from foobar.models import Account +from foobar.tests.factories import AccountFactory, CardFactory + +factory = APIRequestFactory() + + +class MockView(APIView): + def get(self, request): + return HttpResponse({'some': 1, 'thing': 2, 'dandy': 3}) + + def post(self, request): + return HttpResponse({'some': 1, 'thing': 2, 'dandy': 3}) + + def put(self, request): + return HttpResponse({'some': 1, 'thing': 2, 'dandy': 3}) + + +urlpatterns = [ + url(r'^cardtoken/$', + MockView.as_view(authentication_classes=[FooCardAuthentication])), + url(r'^auth-token/$', obtain_foocard_token), +] + + +@override_settings(ROOT_URLCONF='authtoken.test', SECRET_WEBTOKEN='secret') +class AuthenticateFooCardTokenTests(TestCase): + enc = 'HS256' + path = '/cardtoken/' + + def setUp(self): + self.csrf_client = APIClient(enforce_csrf_checks=True) + + def test_correct_auth_of_token(self): + account = AccountFactory() + token = jwt.encode( + {'account_id': str(account.pk)}, + 'secret', + algorithm=self.enc + ) + auth = 'CardToken {0}'.format(token.decode()) + response = self.csrf_client.post( + self.path, + {'example': 'example'}, + format='json', + HTTP_AUTHORIZATION=auth + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_auth_with_invalid_token(self): + token = jwt.encode( + {'some': 'payload'}, + 'invalidsecret', + algorithm=self.enc + ) + auth = 'CardToken {0}'.format(token.decode()) + response = self.csrf_client.post( + self.path, + {'example': 'example'}, + format='json', + HTTP_AUTHORIZATION=auth + ) + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_auth_with_expired_token(self): + stamp = timezone.now() - datetime.timedelta(minutes=10) + token = jwt.encode({'exp': stamp}, 'secret', algorithm=self.enc) + auth = 'CardToken {0}'.format(token.decode()) + response = self.csrf_client.get( + self.path, + HTTP_AUTHORIZATION=auth + ) + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_auth_with_bad_account(self): + AccountFactory() # Keep this here so we just don't have an empty QS + stamp = timezone.now() + datetime.timedelta(minutes=10) + token = jwt.encode( + {'exp': stamp, 'account_id': None}, + 'secret', + algorithm=self.enc + ) + auth = 'CardToken {0}'.format(token.decode()) + response = self.csrf_client.get( + self.path, + {'example': 'example'}, + format='json', + HTTP_AUTHORIZATION=auth + ) + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_auth_with_bad_header(self): + account = AccountFactory() + stamp = timezone.now() + datetime.timedelta(minutes=10) + token = jwt.encode( + {'exp': stamp, 'account_id': str(account.pk)}, + 'secret', + algorithm=self.enc + ) + auth = 'ThisIsIncorrect {0}'.format(token.decode()) + response = self.csrf_client.get( + self.path, + {'exp': stamp, 'account_id': str(account.pk)}, + format='json', + HTTP_AUTHORIZATION=auth + ) + # Returns 200_OK as it is not allowed to even try to authenticate + # And there is no other authentication that is tested + self.assertEqual(response.status_code, status.HTTP_200_OK) + + auth = 'CardToken' + response = self.csrf_client.get( + self.path, + {'exp': stamp, 'account_id': str(account.pk)}, + format='json', + HTTP_AUTHORIZATION=auth + ) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + auth = 'CardToken {0} {1}'.format(token.decode()[:10], token.decode()) + response = self.csrf_client.get( + self.path, + {'exp': stamp, 'account_id': str(account.pk)}, + format='json', + HTTP_AUTHORIZATION=auth + ) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + +@override_settings(ROOT_URLCONF='authtoken.test', SECRET_WEBTOKEN='secret') +class CreateFooCardTokenTests(TestCase): + enc = 'HS256' + path = '/auth-token/' + + def setUp(self): + self.csrf_client = APIClient(enforce_csrf_checks=True) + + def test_create_new_account_token(self): + card = CardFactory() + api_token = Token.objects.create(key='abc123') + auth = 'Token {0}'.format(api_token) + response = self.csrf_client.post( + self.path, + {'number': card.number}, + format='json', + HTTP_AUTHORIZATION=auth + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('card_token', response.data.keys()) + + token = response.data.get('card_token') + self.assertIsNotNone(token) + self.assertTrue(len(token) > 0) + + try: + decoded = jwt.decode(token, 'secret', algorithm=self.enc) + except jwt.exceptions.InvalidTokenError as e: + self.fail(e) + + account = Account.objects.all().first() + self.assertEqual(decoded.get('account_id'), str(account.pk)) + self.assertIn('exp', decoded.keys()) + + def test_create_token_for_invalid_account(self): + api_token = Token.objects.create(key='abc123') + auth = 'Token {0}'.format(api_token) + response = self.csrf_client.post( + self.path, + {'number': '123456'}, + format='json', + HTTP_AUTHORIZATION=auth + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn('number', response.data.keys()) + + def test_create_token_with_incorrect_data(self): + api_token = Token.objects.create(key='abc123') + auth = 'Token {0}'.format(api_token) + response = self.csrf_client.post( + self.path, + {'sumtinwong': '123456'}, + format='json', + HTTP_AUTHORIZATION=auth + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn('number', response.data.keys()) + + response = self.csrf_client.post( + self.path, + {'number': ''}, + format='json', + HTTP_AUTHORIZATION=auth + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn('number', response.data.keys()) + + response = self.csrf_client.post( + self.path, + {'number': None}, + format='json', + HTTP_AUTHORIZATION=auth + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn('number', response.data.keys()) diff --git a/src/authtoken/views.py b/src/authtoken/views.py new file mode 100644 index 0000000..13b07e5 --- /dev/null +++ b/src/authtoken/views.py @@ -0,0 +1,36 @@ +import jwt + +from django.conf import settings +from django.utils import timezone +from rest_framework import parsers, renderers +from rest_framework.response import Response +from rest_framework.views import APIView + +from .serializers import CardTokenSerializer + + +class ObtainFooCardToken(APIView): + parser_classes = ( + parsers.FormParser, + parsers.MultiPartParser, + parsers.JSONParser + ) + renderer_classes = (renderers.JSONRenderer,) + serializer_class = CardTokenSerializer + + def post(self, request, *args, **kwargs): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + account = serializer.validated_data['account'] + + expiration_date = timezone.now() + timezone.timedelta(minutes=5) + token = jwt.encode( + {'exp': expiration_date, 'account_id': str(account.pk)}, + settings.SECRET_WEBTOKEN, + algorithm='HS256' + ) + + return Response({'card_token': token.decode()}) + + +obtain_foocard_token = ObtainFooCardToken.as_view() diff --git a/src/foobar/settings/base.py b/src/foobar/settings/base.py index 3d1080c..e05c1df 100644 --- a/src/foobar/settings/base.py +++ b/src/foobar/settings/base.py @@ -19,6 +19,7 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.getenv('SECRET_KEY', '%^5x9&idy09abn3my1)p+9_g!=aglt4&qog*5ztxwc@xjjp0m%') +SECRET_WEBTOKEN = os.getenv('SECRET_WEBTOKEN', SECRET_KEY) # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.getenv('DEBUG', True) From 8710ef675c26508da40e698eea1ba75d7624b4d2 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Tue, 21 Feb 2017 16:55:53 +0100 Subject: [PATCH 2/9] Added missing requirement --- requirements/base.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/base.txt b/requirements/base.txt index 3855081..c3124f0 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -14,3 +14,4 @@ requests==2.8.1 pytz==2015.7 raven==5.8.1 pynarlivs==0.9.0 +PyJWT==1.4.2 From 990a06861509f0f0348f2abca45bc4c25341d4e3 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Tue, 21 Feb 2017 18:59:24 +0100 Subject: [PATCH 3/9] Now passing the card number in the payload --- src/authtoken/authentication.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/authtoken/authentication.py b/src/authtoken/authentication.py index 41caab3..a886a5b 100644 --- a/src/authtoken/authentication.py +++ b/src/authtoken/authentication.py @@ -6,7 +6,7 @@ from rest_framework.authentication import get_authorization_header from .models import Token -from foobar.models import Account +from foobar.api import get_account def validate_header(auth): @@ -82,7 +82,6 @@ class FooCardAuthentication(authentication.BaseAuthentication): """ algorithm = 'HS256' message = 'Invalid token' - model = Account def authenticate(self, request): auth = get_authorization_header(request).split() @@ -103,10 +102,9 @@ def authenticate(self, request): return self.authenticate_credentials(payload) def authenticate_credentials(self, payload): - account_id = payload.get('account_id') - try: - account = self.model.objects.get(pk=account_id) - except self.model.DoesNotExist: + card_id = payload.get('card_id') + account = get_account(card_id) + if account is None: raise exceptions.AuthenticationFailed(_(self.message)) return (account, None) From b20a85f05703a7cfc9f250347dc07dc5f1456d45 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Tue, 21 Feb 2017 19:00:37 +0100 Subject: [PATCH 4/9] Validating card number on field level, with help from the internal API --- src/authtoken/serializers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/authtoken/serializers.py b/src/authtoken/serializers.py index 12131e7..d7ff694 100644 --- a/src/authtoken/serializers.py +++ b/src/authtoken/serializers.py @@ -7,11 +7,10 @@ class CardTokenSerializer(serializers.Serializer): number = serializers.CharField(required=True, allow_blank=False) - def validate(self, attrs): - account = get_account(attrs.get('number')) + def validate_number(self, value): + account = get_account(value) if account is None: msg = {'number': _('Card is not registered')} raise serializers.ValidationError(msg) - attrs['account'] = account - return attrs + return value From 9f717b838d5a72eac036c9bc63da599783c9909a Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Tue, 21 Feb 2017 20:00:03 +0100 Subject: [PATCH 5/9] Removed parser and renderer classes. Also putting card_id instead of account_id in payload --- src/authtoken/views.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/authtoken/views.py b/src/authtoken/views.py index 13b07e5..006e05e 100644 --- a/src/authtoken/views.py +++ b/src/authtoken/views.py @@ -2,7 +2,6 @@ from django.conf import settings from django.utils import timezone -from rest_framework import parsers, renderers from rest_framework.response import Response from rest_framework.views import APIView @@ -10,22 +9,16 @@ class ObtainFooCardToken(APIView): - parser_classes = ( - parsers.FormParser, - parsers.MultiPartParser, - parsers.JSONParser - ) - renderer_classes = (renderers.JSONRenderer,) serializer_class = CardTokenSerializer def post(self, request, *args, **kwargs): serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True) - account = serializer.validated_data['account'] + number = serializer.validated_data['number'] expiration_date = timezone.now() + timezone.timedelta(minutes=5) token = jwt.encode( - {'exp': expiration_date, 'account_id': str(account.pk)}, + {'exp': expiration_date, 'card_id': number}, settings.SECRET_WEBTOKEN, algorithm='HS256' ) From 65d5039aa2363387158b8c3f5c770485e52233ee Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Tue, 21 Feb 2017 20:01:33 +0100 Subject: [PATCH 6/9] Updated tests to utilize mock data in library responses. Also removed tests actually testing those libraries --- src/authtoken/test.py | 129 +++++++++--------------------------------- 1 file changed, 28 insertions(+), 101 deletions(-) diff --git a/src/authtoken/test.py b/src/authtoken/test.py index 9f014d0..cf24943 100644 --- a/src/authtoken/test.py +++ b/src/authtoken/test.py @@ -1,5 +1,6 @@ import datetime import jwt +from unittest.mock import patch from django.conf.urls import url from django.http import HttpResponse @@ -12,7 +13,6 @@ from .authentication import FooCardAuthentication from .models import Token from .views import obtain_foocard_token -from foobar.models import Account from foobar.tests.factories import AccountFactory, CardFactory factory = APIRequestFactory() @@ -44,30 +44,19 @@ class AuthenticateFooCardTokenTests(TestCase): def setUp(self): self.csrf_client = APIClient(enforce_csrf_checks=True) - def test_correct_auth_of_token(self): - account = AccountFactory() + @patch('authtoken.authentication.jwt.decode') + def test_correct_auth_of_token(self, mock_jwt): + card = CardFactory() token = jwt.encode( - {'account_id': str(account.pk)}, + {'card_id': card.number}, 'secret', algorithm=self.enc ) - auth = 'CardToken {0}'.format(token.decode()) - response = self.csrf_client.post( - self.path, - {'example': 'example'}, - format='json', - HTTP_AUTHORIZATION=auth - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) + token = token.decode() + stamp = timezone.now() + timezone.timedelta(minutes=5) + mock_jwt.return_value = {'exp': stamp, 'card_id': card.number} - def test_auth_with_invalid_token(self): - token = jwt.encode( - {'some': 'payload'}, - 'invalidsecret', - algorithm=self.enc - ) - auth = 'CardToken {0}'.format(token.decode()) + auth = 'CardToken {0}'.format(token) response = self.csrf_client.post( self.path, {'example': 'example'}, @@ -75,72 +64,47 @@ def test_auth_with_invalid_token(self): HTTP_AUTHORIZATION=auth ) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - def test_auth_with_expired_token(self): - stamp = timezone.now() - datetime.timedelta(minutes=10) - token = jwt.encode({'exp': stamp}, 'secret', algorithm=self.enc) - auth = 'CardToken {0}'.format(token.decode()) - response = self.csrf_client.get( - self.path, - HTTP_AUTHORIZATION=auth - ) - - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(response.status_code, status.HTTP_200_OK) + mock_jwt.assert_called_once_with(token, 'secret', algorithm=self.enc) - def test_auth_with_bad_account(self): + @patch('authtoken.authentication.jwt.decode') + def test_auth_with_bad_account(self, mock_jwt): AccountFactory() # Keep this here so we just don't have an empty QS stamp = timezone.now() + datetime.timedelta(minutes=10) token = jwt.encode( - {'exp': stamp, 'account_id': None}, + {'exp': stamp, 'card_id': None}, 'secret', algorithm=self.enc ) - auth = 'CardToken {0}'.format(token.decode()) - response = self.csrf_client.get( - self.path, - {'example': 'example'}, - format='json', - HTTP_AUTHORIZATION=auth - ) + token = token.decode() + mock_jwt.return_value = {'exp': stamp, 'card_id': None} + + auth = 'CardToken {0}'.format(token) + response = self.csrf_client.get(self.path, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + mock_jwt.assert_called_once_with(token, 'secret', algorithm=self.enc) def test_auth_with_bad_header(self): account = AccountFactory() stamp = timezone.now() + datetime.timedelta(minutes=10) token = jwt.encode( - {'exp': stamp, 'account_id': str(account.pk)}, + {'exp': stamp, 'card_id': str(account.pk)}, 'secret', algorithm=self.enc ) auth = 'ThisIsIncorrect {0}'.format(token.decode()) - response = self.csrf_client.get( - self.path, - {'exp': stamp, 'account_id': str(account.pk)}, - format='json', - HTTP_AUTHORIZATION=auth - ) + response = self.csrf_client.get(self.path, HTTP_AUTHORIZATION=auth) # Returns 200_OK as it is not allowed to even try to authenticate # And there is no other authentication that is tested self.assertEqual(response.status_code, status.HTTP_200_OK) auth = 'CardToken' - response = self.csrf_client.get( - self.path, - {'exp': stamp, 'account_id': str(account.pk)}, - format='json', - HTTP_AUTHORIZATION=auth - ) + response = self.csrf_client.get(self.path, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) auth = 'CardToken {0} {1}'.format(token.decode()[:10], token.decode()) - response = self.csrf_client.get( - self.path, - {'exp': stamp, 'account_id': str(account.pk)}, - format='json', - HTTP_AUTHORIZATION=auth - ) + response = self.csrf_client.get(self.path, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) @@ -155,6 +119,7 @@ def setUp(self): def test_create_new_account_token(self): card = CardFactory() api_token = Token.objects.create(key='abc123') + auth = 'Token {0}'.format(api_token) response = self.csrf_client.post( self.path, @@ -170,14 +135,9 @@ def test_create_new_account_token(self): self.assertIsNotNone(token) self.assertTrue(len(token) > 0) - try: - decoded = jwt.decode(token, 'secret', algorithm=self.enc) - except jwt.exceptions.InvalidTokenError as e: - self.fail(e) - - account = Account.objects.all().first() - self.assertEqual(decoded.get('account_id'), str(account.pk)) - self.assertIn('exp', decoded.keys()) + auth = 'CardToken {0}'.format(token) + response = self.csrf_client.get('/cardtoken/', HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, status.HTTP_200_OK) def test_create_token_for_invalid_account(self): api_token = Token.objects.create(key='abc123') @@ -191,36 +151,3 @@ def test_create_token_for_invalid_account(self): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertIn('number', response.data.keys()) - - def test_create_token_with_incorrect_data(self): - api_token = Token.objects.create(key='abc123') - auth = 'Token {0}'.format(api_token) - response = self.csrf_client.post( - self.path, - {'sumtinwong': '123456'}, - format='json', - HTTP_AUTHORIZATION=auth - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertIn('number', response.data.keys()) - - response = self.csrf_client.post( - self.path, - {'number': ''}, - format='json', - HTTP_AUTHORIZATION=auth - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertIn('number', response.data.keys()) - - response = self.csrf_client.post( - self.path, - {'number': None}, - format='json', - HTTP_AUTHORIZATION=auth - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertIn('number', response.data.keys()) From ec7d4c87e321bd4e01637226a08247f8920e7fcd Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Tue, 21 Feb 2017 20:02:13 +0100 Subject: [PATCH 7/9] Corrected type of card factory field value --- src/foobar/tests/factories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foobar/tests/factories.py b/src/foobar/tests/factories.py index 7e984e2..56bbcb4 100644 --- a/src/foobar/tests/factories.py +++ b/src/foobar/tests/factories.py @@ -12,4 +12,4 @@ class Meta: model = models.Card account = factory.SubFactory(AccountFactory) - number = factory.fuzzy.FuzzyInteger(0, (1 << 32) - 1) + number = str(factory.fuzzy.FuzzyInteger(0, (1 << 32) - 1)) From 99e4a8670b3be9b5171b9378be59fb34ea3094dc Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Tue, 21 Feb 2017 20:25:19 +0100 Subject: [PATCH 8/9] Changed the factory for cards to have a lazy_attribute instead of FuzzyInteger --- src/foobar/tests/factories.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/foobar/tests/factories.py b/src/foobar/tests/factories.py index 56bbcb4..b05371e 100644 --- a/src/foobar/tests/factories.py +++ b/src/foobar/tests/factories.py @@ -1,3 +1,5 @@ +import random + import factory.fuzzy from .. import models @@ -12,4 +14,8 @@ class Meta: model = models.Card account = factory.SubFactory(AccountFactory) - number = str(factory.fuzzy.FuzzyInteger(0, (1 << 32) - 1)) + + @factory.lazy_attribute + def number(self): + value = random.randint(0, (1 << 32) - 1) + return str(value) From 294e908433d8e8b972ab78b8ed6b32183e7df7c0 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Tue, 21 Feb 2017 20:38:45 +0100 Subject: [PATCH 9/9] Added test for authorizing with invalid token --- src/authtoken/test.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/authtoken/test.py b/src/authtoken/test.py index cf24943..648a9ba 100644 --- a/src/authtoken/test.py +++ b/src/authtoken/test.py @@ -1,5 +1,6 @@ import datetime import jwt +from jwt.exceptions import InvalidTokenError from unittest.mock import patch from django.conf.urls import url @@ -25,9 +26,6 @@ def get(self, request): def post(self, request): return HttpResponse({'some': 1, 'thing': 2, 'dandy': 3}) - def put(self, request): - return HttpResponse({'some': 1, 'thing': 2, 'dandy': 3}) - urlpatterns = [ url(r'^cardtoken/$', @@ -67,6 +65,18 @@ def test_correct_auth_of_token(self, mock_jwt): self.assertEqual(response.status_code, status.HTTP_200_OK) mock_jwt.assert_called_once_with(token, 'secret', algorithm=self.enc) + @patch('authtoken.authentication.jwt.decode') + def test_auth_with_bad_token(self, mock_jwt): + token = '401f7ac837da42b97f613d789819ff93537bee6a' + auth = 'CardToken {0}'.format(token) + + mock_jwt.side_effect = InvalidTokenError('Invalid token') + + response = self.csrf_client.get(self.path, HTTP_AUTHORIZATION=auth) + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + mock_jwt.assert_called_once_with(token, 'secret', algorithm=self.enc) + @patch('authtoken.authentication.jwt.decode') def test_auth_with_bad_account(self, mock_jwt): AccountFactory() # Keep this here so we just don't have an empty QS