diff --git a/.travis.yml b/.travis.yml index e4bce7c8..1c1ffe5a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ -python: '3.5' - language: python cache: pip @@ -27,12 +25,6 @@ env: - TOX_ENV=py27-django1.11-drf3.4 - TOX_ENV=py27-django1.11-drf3.5 - - TOX_ENV=py33-django1.8-drf3.1 - - TOX_ENV=py33-django1.8-drf3.2 - - TOX_ENV=py33-django1.8-drf3.3 - - TOX_ENV=py33-django1.8-drf3.4 - - TOX_ENV=py33-django1.8-drf3.5 - - TOX_ENV=py34-django1.8-drf3.1 - TOX_ENV=py34-django1.8-drf3.2 - TOX_ENV=py34-django1.8-drf3.3 diff --git a/requirements/testing.txt b/requirements/testing.txt index 3a666e9c..30605bff 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -2,6 +2,7 @@ pytest==2.6.4 pytest-django==2.8.0 pytest-cov==1.6 +freezegun==0.3.9 # Mocking the datetime module. cryptography==1.2.3 diff --git a/rest_framework_jwt/migrations/0001_initial.py b/rest_framework_jwt/migrations/0001_initial.py new file mode 100644 index 00000000..a89e96f4 --- /dev/null +++ b/rest_framework_jwt/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2017-07-21 08:06 +from __future__ import unicode_literals +import uuid + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Device', + fields=[ + ('permanent_token', models.CharField(max_length=255, unique=True, serialize=False)), + ('jwt_secret', models.UUIDField(default=uuid.uuid4, editable=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('name', models.CharField(max_length=255, verbose_name='Device name')), + ('details', models.CharField(blank=True, max_length=255, verbose_name='Device details')), + ('last_request_datetime', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/rest_framework_jwt/migrations/__init__.py b/rest_framework_jwt/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rest_framework_jwt/models.py b/rest_framework_jwt/models.py index 5b53a526..9d384e8f 100644 --- a/rest_framework_jwt/models.py +++ b/rest_framework_jwt/models.py @@ -1 +1,29 @@ -# Just to keep things like ./manage.py test happy +import binascii +import os +import uuid + +from django.conf import settings +from django.db import models +from django.utils.translation import ugettext_lazy as _ + + +class Device(models.Model): + """ + Device model used for permanent token authentication + """ + permanent_token = models.CharField(max_length=255, unique=True) + jwt_secret = models.UUIDField(default=uuid.uuid4, editable=False) + created = models.DateTimeField(auto_now_add=True) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + name = models.CharField(_('Device name'), max_length=255) + details = models.CharField(_('Device details'), max_length=255, blank=True) + last_request_datetime = models.DateTimeField(auto_now=True) + + def save(self, *args, **kwargs): + if not self.permanent_token: + self.permanent_token = self.generate_key() + + return super(Device, self).save(*args, **kwargs) + + def generate_key(self): + return binascii.hexlify(os.urandom(20)).decode() diff --git a/rest_framework_jwt/serializers.py b/rest_framework_jwt/serializers.py index 12b10a44..e181e999 100644 --- a/rest_framework_jwt/serializers.py +++ b/rest_framework_jwt/serializers.py @@ -8,6 +8,7 @@ from rest_framework import serializers from .compat import Serializer +from rest_framework_jwt.models import Device from rest_framework_jwt.settings import api_settings from rest_framework_jwt.compat import get_username_field, PasswordField @@ -54,12 +55,29 @@ def validate(self, attrs): msg = _('User account is disabled.') raise serializers.ValidationError(msg) - payload = jwt_payload_handler(user) - - return { - 'token': jwt_encode_handler(payload), - 'user': user - } + data = {} + device = None + if api_settings.JWT_PERMANENT_TOKEN_AUTH: + headers = self.context['request'].META + device_name = headers.get('HTTP_X_DEVICE_MODEL') + user_agent = headers.get('HTTP_USER_AGENT', '') + if not device_name: + device_name = user_agent + device_details = '' + else: + device_details = user_agent + + device = Device.objects.create( + user=user, last_request_datetime=datetime.now(), + name=device_name, details=device_details) + data['device'] = device + + payload = jwt_payload_handler(user, device=device) + data.update({ + 'user': user, + 'token': jwt_encode_handler(payload) + }) + return data else: msg = _('Unable to log in with provided credentials.') raise serializers.ValidationError(msg) @@ -169,3 +187,35 @@ def validate(self, attrs): 'token': jwt_encode_handler(new_payload), 'user': user } + + +class DeviceSerializer(serializers.ModelSerializer): + class Meta: + model = Device + fields = ['id', 'created', 'name', 'details', 'last_request_datetime'] + + +class DeviceTokenRefreshSerializer(Serializer): + permanent_token = serializers.CharField(required=True) + + def validate(self, attrs): + permanent_token = attrs['permanent_token'] + try: + device = Device.objects.get(permanent_token=permanent_token) + except Device.DoesNotExist: + raise serializers.ValidationError({'permanent_token': _('Invalid permanent_token value.')}) + + now = datetime.now() + if now > device.last_request_datetime + api_settings.JWT_PERMANENT_TOKEN_EXPIRATION_DELTA: + device.delete() + raise serializers.ValidationError({'permanent_token': _('Permanent token has expired.')}) + + if now > device.last_request_datetime + api_settings.JWT_PERMANENT_TOKEN_EXPIRATION_ACCURACY: + device.last_request_datetime = now + device.save() + + payload = jwt_payload_handler(device.user, device=device) + return { + 'token': jwt_encode_handler(payload), + 'user': device.user + } diff --git a/rest_framework_jwt/settings.py b/rest_framework_jwt/settings.py index e47320bb..0fb5517a 100644 --- a/rest_framework_jwt/settings.py +++ b/rest_framework_jwt/settings.py @@ -46,6 +46,10 @@ 'JWT_AUTH_HEADER_PREFIX': 'JWT', 'JWT_AUTH_COOKIE': None, + + 'JWT_PERMANENT_TOKEN_AUTH': False, + 'JWT_PERMANENT_TOKEN_EXPIRATION_ACCURACY': datetime.timedelta(minutes=30), + 'JWT_PERMANENT_TOKEN_EXPIRATION_DELTA': datetime.timedelta(days=7) } # List of settings that may be in string import notation. diff --git a/rest_framework_jwt/utils.py b/rest_framework_jwt/utils.py index c72197bc..30e0dda7 100644 --- a/rest_framework_jwt/utils.py +++ b/rest_framework_jwt/utils.py @@ -9,6 +9,7 @@ from rest_framework_jwt.compat import get_username from rest_framework_jwt.compat import get_username_field +from rest_framework_jwt.models import Device from rest_framework_jwt.settings import api_settings @@ -21,6 +22,10 @@ def jwt_get_secret_key(payload=None): - password is changed - etc. """ + if api_settings.JWT_PERMANENT_TOKEN_AUTH: + device = Device.objects.get(pk=payload.get('device_id')) + return device.jwt_secret.hex + if api_settings.JWT_GET_USER_SECRET_KEY: User = get_user_model() # noqa: N806 user = User.objects.get(pk=payload.get('user_id')) @@ -29,7 +34,7 @@ def jwt_get_secret_key(payload=None): return api_settings.JWT_SECRET_KEY -def jwt_payload_handler(user): +def jwt_payload_handler(user, device=None): username_field = get_username_field() username = get_username(user) @@ -64,6 +69,9 @@ def jwt_payload_handler(user): if api_settings.JWT_ISSUER is not None: payload['iss'] = api_settings.JWT_ISSUER + if api_settings.JWT_PERMANENT_TOKEN_AUTH: + payload['device_id'] = str(device.pk) + return payload @@ -88,7 +96,11 @@ def jwt_get_username_from_payload_handler(payload): def jwt_encode_handler(payload): - key = api_settings.JWT_PRIVATE_KEY or jwt_get_secret_key(payload) + if api_settings.JWT_PERMANENT_TOKEN_AUTH or not api_settings.JWT_PRIVATE_KEY: + key = jwt_get_secret_key(payload) + else: + key = api_settings.JWT_PRIVATE_KEY + return jwt.encode( payload, key, @@ -115,7 +127,7 @@ def jwt_decode_handler(token): ) -def jwt_response_payload_handler(token, user=None, request=None): +def jwt_response_payload_handler(token, user=None, request=None, **kwargs): """ Returns the response data for both the login and refresh views. Override to return a custom response such as including the @@ -123,13 +135,22 @@ def jwt_response_payload_handler(token, user=None, request=None): Example: - def jwt_response_payload_handler(token, user=None, request=None): + def jwt_response_payload_handler(token, user=None, request=None, **kwargs): return { 'token': token, 'user': UserSerializer(user, context={'request': request}).data } """ - return { + data = { 'token': token } + if api_settings.JWT_PERMANENT_TOKEN_AUTH: + permanent_token = kwargs.get('permanent_token') + if permanent_token: + data['permanent_token'] = permanent_token + device_id = kwargs.get('device_id') + if device_id: + data['device_id'] = device_id + + return data diff --git a/rest_framework_jwt/views.py b/rest_framework_jwt/views.py index 30cd4646..9df0efe5 100644 --- a/rest_framework_jwt/views.py +++ b/rest_framework_jwt/views.py @@ -1,18 +1,40 @@ +from django.utils.encoding import force_text +from django.utils.translation import ugettext_lazy as _ +from rest_framework import mixins, status, viewsets +from rest_framework.exceptions import APIException, NotFound +from rest_framework.generics import DestroyAPIView from rest_framework.views import APIView -from rest_framework import status +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from datetime import datetime +from .models import Device from .settings import api_settings from .serializers import ( - JSONWebTokenSerializer, RefreshJSONWebTokenSerializer, + DeviceSerializer, DeviceTokenRefreshSerializer, JSONWebTokenSerializer, RefreshJSONWebTokenSerializer, VerifyJSONWebTokenSerializer ) jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER -class JSONWebTokenAPIView(APIView): +class HeaderDisallowed(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _('Using the {header} header is disallowed for {view_name}.') + + def __init__(self, header, view_name, detail=None): + self.detail = force_text(self.default_detail).format(header=header, view_name=view_name) + + +class HeadersCheckMixin(object): + def initial(self, request, *args, **kwargs): + if (api_settings.JWT_PERMANENT_TOKEN_AUTH and request.META.get('permanent_token') and + type(self) != DeviceRefreshJSONWebToken): + raise HeaderDisallowed('permanent_token', type(self).__name__) + super(HeadersCheckMixin, self).initial(request, *args, **kwargs) + + +class JSONWebTokenAPIView(HeadersCheckMixin, APIView): """ Base API View that various JWT interactions inherit from. """ @@ -57,7 +79,12 @@ def post(self, request, *args, **kwargs): if serializer.is_valid(): user = serializer.object.get('user') or request.user token = serializer.object.get('token') - response_data = jwt_response_payload_handler(token, user, request) + device = serializer.object.get('device', None) + kwargs = {} + if device: + kwargs.update(dict(permanent_token=device.permanent_token, device_id=device.id)) + + response_data = jwt_response_payload_handler(token, user, request, **kwargs) response = Response(response_data) if api_settings.JWT_AUTH_COOKIE: expiration = (datetime.utcnow() + @@ -99,6 +126,44 @@ class RefreshJSONWebToken(JSONWebTokenAPIView): serializer_class = RefreshJSONWebTokenSerializer +class DeviceRefreshJSONWebToken(HeadersCheckMixin, APIView): + """ + API View used to refresh JSON Web Token using permanent token. + """ + serializer_class = DeviceTokenRefreshSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.META) + if serializer.is_valid(raise_exception=True): + data = jwt_response_payload_handler(request=request, **serializer.validated_data) + return Response(data, status=status.HTTP_200_OK) + + +class DeviceLogout(HeadersCheckMixin, DestroyAPIView): + """ + Logout user by deleting Device. + """ + queryset = Device.objects.all() + permission_classes = [IsAuthenticated] + + def get_object(self): + try: + return self.get_queryset().get(user=self.request.user, id=self.request.META.get('device_id')) + except Device.DoesNotExist: + raise NotFound(_('Device does not exist.')) + + +class DeviceViewSet(HeadersCheckMixin, mixins.ListModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet): + queryset = Device.objects.all() + serializer_class = DeviceSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return self.queryset.filter(user=self.request.user) + + obtain_jwt_token = ObtainJSONWebToken.as_view() refresh_jwt_token = RefreshJSONWebToken.as_view() verify_jwt_token = VerifyJSONWebToken.as_view() +device_refresh_token = DeviceRefreshJSONWebToken.as_view() +device_logout = DeviceLogout.as_view() diff --git a/tests/conftest.py b/tests/conftest.py index fe079bc7..70ddbd9a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,11 +35,18 @@ def pytest_configure(): 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', + 'rest_framework_jwt', 'tests', ), PASSWORD_HASHERS=( 'django.contrib.auth.hashers.MD5PasswordHasher', ), + REST_FRAMEWORK={ + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework_jwt.authentication.JSONWebTokenAuthentication' + ] + } ) try: diff --git a/tests/test_views.py b/tests/test_views.py index c8c72465..0ff65ab1 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -8,11 +8,13 @@ from django import get_version from django.test import TestCase from django.test.utils import override_settings +from freezegun import freeze_time from rest_framework import status from rest_framework.test import APIClient from rest_framework_jwt import utils, views from rest_framework_jwt.compat import get_user_model +from rest_framework_jwt.models import Device from rest_framework_jwt.settings import api_settings, DEFAULTS from . import utils as test_utils @@ -79,6 +81,7 @@ def test_jwt_login_json(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(decoded_payload['username'], self.username) + self.assertNotIn('permanent_token', response.data) def test_jwt_login_json_bad_creds(self): """ @@ -150,6 +153,45 @@ def test_jwt_login_using_zero(self): self.assertEqual(response.status_code, 400) + def test_jwt_permanent_token_auth(self): + api_settings.JWT_PERMANENT_TOKEN_AUTH = True + + client = APIClient() + client.credentials(HTTP_X_DEVICE_MODEL='Nokia', HTTP_USER_AGENT='agent') + self.assertEqual(Device.objects.all().count(), 0) + response = client.post('/auth-token/', self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(set(response.data.keys()), {'token', 'permanent_token', 'device_id'}) + device = Device.objects.get(permanent_token=response.data['permanent_token']) + self.assertEqual(response.data['device_id'], device.id) + self.assertIsNotNone(response.data['token']) + self.assertEqual(device.name, 'Nokia') + self.assertEqual(device.details, 'agent') + self.assertEqual(Device.objects.all().count(), 1) + Device.objects.all().delete() + + # test using without setting device model - for example on browser + client.credentials(HTTP_USER_AGENT='agent') + self.assertEqual(Device.objects.all().count(), 0) + response = client.post('/auth-token/', self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + token = response.data['token'] + device = Device.objects.get(permanent_token=response.data['permanent_token']) + self.assertEqual(response.data['device_id'], device.id) + self.assertEqual(device.name, 'agent') + self.assertEqual(device.details, '') + self.assertEqual(Device.objects.all().count(), 1) + self.assertEqual(Device.objects.get(permanent_token=response.data['permanent_token']).name, 'agent') + + # check if the generated token works + client.credentials(HTTP_AUTHORIZATION='JWT {}'.format(token)) + client.login(**self.data) + response = client.get('/devices/', format='json') + self.assertEqual(response.status_code, 200) + + def tearDown(self): + api_settings.JWT_PERMANENT_TOKEN_AUTH = False + @unittest.skipIf(get_version() < '1.5.0', 'No Configurable User model feature') @override_settings(AUTH_USER_MODEL='tests.CustomUser') @@ -492,3 +534,192 @@ def test_refresh_jwt_after_refresh_expiration(self): def tearDown(self): # Restore original settings api_settings.JWT_ALLOW_REFRESH = DEFAULTS['JWT_ALLOW_REFRESH'] + + +class DeviceLogoutViewTests(BaseTestCase): + def setUp(self): + super(DeviceLogoutViewTests, self).setUp() + self.second_user = User.objects.create_user( + self.username + '2', self.email + '2', self.password) + + api_settings.JWT_PERMANENT_TOKEN_AUTH = True + + def test_logout_view(self): + client = APIClient(enforce_csrf_checks=True) + + # create device + headers = {'HTTP_X_DEVICE_MODEL': 'Android 123'} + client.credentials(**headers) + response = client.post('/auth-token/', self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Device.objects.all().count(), 1) + device_id = response.data['device_id'] + + headers['HTTP_AUTHORIZATION'] = 'JWT {}'.format(response.data['token']) + headers['device_id'] = device_id + client.credentials(**headers) + client.login(**self.data) + response = client.delete('/device-logout/', format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Device.objects.all().count(), 0) + + def test_logout_unknown_device(self): + client = APIClient(enforce_csrf_checks=True) + + # create a few devices + headers = {'HTTP_X_DEVICE_MODEL': 'Android 123'} + client.credentials(**headers) + response = client.post('/auth-token/', self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + token = response.data['token'] + + headers['HTTP_X_DEVICE_MODEL'] = 'Nokia' + client.credentials(**headers) + response = client.post('/auth-token/', {'username': self.second_user.username, 'password': self.password}, + format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Device.objects.all().count(), 2) + device_id = response.data['device_id'] + + headers['HTTP_AUTHORIZATION'] = 'JWT {}'.format(token) + headers['device_id'] = device_id + client.credentials(**headers) + client.login(**self.data) + response = client.delete('/device-logout/', format='json') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(Device.objects.all().count(), 2) + + def tearDown(self): + api_settings.JWT_PERMANENT_TOKEN_AUTH = False + + +class DeviceRefreshTokenViewsTests(BaseTestCase): + def setUp(self): + super(DeviceRefreshTokenViewsTests, self).setUp() + api_settings.JWT_PERMANENT_TOKEN_AUTH = True + + def test_refreshing(self): + with freeze_time('2016-01-01 00:00:00') as frozen_time: + client = APIClient(enforce_csrf_checks=True) + + headers = {'HTTP_X_DEVICE_MODEL': 'Android 123'} + client.credentials(**headers) + response = client.post('/auth-token/', self.data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + permanent_token = response.data['permanent_token'] + old_token = response.data['token'] + + frozen_time.tick(delta=timedelta(days=2)) + # test w/o passing permanent_token + response = client.post('/device-refresh-token/', format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # test passing permanent token that does not exist in the database + fake_permanent_token = '23124csfdgfdhthfdfdf' + self.assertEqual(Device.objects.filter(permanent_token=fake_permanent_token).count(), 0) + headers['permanent_token'] = fake_permanent_token + client.credentials(**headers) + response = client.post('/device-refresh-token/', format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + headers['permanent_token'] = permanent_token + client.credentials(**headers) + response = client.post('/device-refresh-token/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(set(response.data.keys()), {'token'}) + device = Device.objects.get(permanent_token=permanent_token) + self.assertEqual(device.last_request_datetime, datetime.now()) + token = response.data['token'] + self.assertNotEqual(token, old_token) + + # test auth with the new token + client.credentials(HTTP_AUTHORIZATION='JWT {}'.format(token)) + client.login(**self.data) + response = client.get('/devices/') + self.assertEqual(response.status_code, 200) + + # test permanent token expiration + frozen_time.tick(delta=timedelta(days=8)) + client.credentials(**headers) + response = client.post('/device-refresh-token/', format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + with self.assertRaises(Device.DoesNotExist): + Device.objects.get(permanent_token=permanent_token) + + def tearDown(self): + api_settings.JWT_PERMANENT_TOKEN_AUTH = False + + +class DeviceViewTests(TokenTestCase): + def setUp(self): + super(DeviceViewTests, self).setUp() + self.device = Device.objects.create( + user=self.user, permanent_token='somestring2', name='Android', + last_request_datetime=datetime.now()) + self.user2 = User.objects.create_user(email='jsmith@example.com', username='jsmith', password='password') + self.device2 = Device.objects.create( + user=self.user2, permanent_token='somestring98', name='Android', + last_request_datetime=datetime.now()) + + def _login(self, client): + client.credentials(HTTP_AUTHORIZATION='JWT {}'.format(self.get_token())) + return client.login(**self.data) + + def test_device_delete(self): + client = APIClient() + # test accessing without being logged in + response = client.delete('/devices/{}/'.format(self.device.id)) + self.assertEqual(response.status_code, 401) + + self._login(client) + # try removing device linked to other user + response = client.delete('/devices/{}/'.format(self.device2.id)) + self.assertEqual(response.status_code, 404) + # test regular case + self.assertEqual(Device.objects.filter(id=self.device.id).count(), 1) + response = client.delete('/devices/{}/'.format(self.device.id)) + self.assertEqual(response.status_code, 204) + self.assertEqual(Device.objects.filter(id=self.device.id).count(), 0) + + def test_device_list(self): + client = APIClient() + self._login(client) + response = client.get('/devices/', format='json') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 1) + self.assertEqual(set(response.data[0].keys()), { + 'id', 'created', 'name', 'details', 'last_request_datetime' + }) + self.assertEqual(response.data[0]['id'], self.device.id) + + +class HeadersCheckViewMixinTests(BaseTestCase): + def setUp(self): + super(HeadersCheckViewMixinTests, self).setUp() + api_settings.JWT_PERMANENT_TOKEN_AUTH = True + + def test_disallowing_permanent_token(self): + client = APIClient(enforce_csrf_checks=True) + client.credentials(permanent_token='123') + urls = [ + '/auth-token/', + '/auth-token-refresh/', + '/auth-token-verify/', + '/device-logout/', + '/devices/', + '/devices/1/' + ] + for url in urls: + # request method makes no difference here, as the check is done on dispatch + response = client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + allowed_urls = [ + '/device-refresh-token/' + ] + for url in allowed_urls: + response = client.get(url, format='json') + self.assertNotEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def tearDown(self): + api_settings.JWT_PERMANENT_TOKEN_AUTH = False diff --git a/tests/urls.py b/tests/urls.py index 2d5e6c32..9e2dc5a3 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,6 +1,6 @@ from django.conf.urls import url from django.http import HttpResponse -from rest_framework import permissions +from rest_framework import permissions, routers from rest_framework.views import APIView try: from rest_framework_oauth.authentication import OAuth2Authentication @@ -24,10 +24,15 @@ def post(self, request): return HttpResponse({'a': 1, 'b': 2, 'c': 3}) +router = routers.SimpleRouter() +router.register(r'devices', views.DeviceViewSet) + urlpatterns = [ url(r'^auth-token/$', views.obtain_jwt_token), url(r'^auth-token-refresh/$', views.refresh_jwt_token), url(r'^auth-token-verify/$', views.verify_jwt_token), + url(r'^device-refresh-token/$', views.device_refresh_token), + url(r'^device-logout/$', views.device_logout), url(r'^jwt/$', MockView.as_view( authentication_classes=[JSONWebTokenAuthentication])), @@ -37,4 +42,4 @@ def post(self, request): url(r'^oauth2-jwt/$', MockView.as_view( authentication_classes=[ OAuth2Authentication, JSONWebTokenAuthentication])), -] +] + router.urls diff --git a/tests/utils.py b/tests/utils.py index a72529bb..a7c2d4e5 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,7 @@ from rest_framework_jwt.compat import get_username -def jwt_response_payload_handler(token, user=None, request=None): +def jwt_response_payload_handler(token, user=None, request=None, **kwargs): """ Returns the response data for both the login and refresh views. Override to return a custom response such as including the @@ -9,7 +9,7 @@ def jwt_response_payload_handler(token, user=None, request=None): Example: - def jwt_response_payload_handler(token, user=None, request=None): + def jwt_response_payload_handler(token, user=None, request=None, **kwargs): return { 'token': token, 'user': UserSerializer(user, context={'request': request}).data diff --git a/tox.ini b/tox.ini index 09de4372..90da796d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = py27-{flake8,docs}, - {py27,py33,py34,py35}-django{1.8,1.9,1.10,1.11}-drf{3.1,3.2,3.3,3.4,3.5} + {py27,py34,py35,py36}-django{1.8,1.9,1.10,1.11}-drf{3.1,3.2,3.3,3.4,3.5} [testenv] commands = ./runtests.py --fast {posargs} --verbose